Compare commits

..

20 Commits

Author SHA1 Message Date
Kai 53f2be2b4c fix merge
Changelog: changed
2025-08-19 10:22:31 -04:00
Kai 11d4ec383e fix merge
Changelog: changed
2025-08-18 11:21:25 -04:00
Kai 493d77b43b Merge branch 'master' into simple-motion-layout
# Conflicts:
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
#	app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
#	app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
#	app/src/main/res/layout/video_view.xml
#	app/src/stable/assets/sources/apple-podcasts
#	app/src/unstable/assets/sources/apple-podcasts
2025-08-18 11:05:23 -04:00
Kelvin 1edc8aabf8 Fix login dialog 2025-08-15 21:20:23 +02:00
Kelvin 91060faac9 VOD chat 2025-08-15 16:36:38 +02:00
Kelvin 17027ba364 Remote history sync on toggle 2025-08-14 21:03:39 +02:00
Kelvin 8569eaa5db Hide DevSubmit filter 2025-08-14 20:36:56 +02:00
Kelvin d32d817e0a Merge branch 'shorts-improv' into 'master'
Fix background play, disable artwork on background till improved, renamed...

See merge request videostreaming/grayjay!140
2025-08-14 11:26:47 +00:00
Kelvin a0f4cc760c Fix background play, disable artwork on background till improved, renamed variable that caused confusion 2025-08-14 12:35:46 +02:00
Kelvin 5247997ea5 Set plugin install request timeouts, fix messaging surrounding downloading icons 2025-08-13 19:36:26 +02:00
Kelvin 453030d561 Merge branch 'shorts-improv' into 'master'
Various shorts improvements, login warnings support, etc

See merge request videostreaming/grayjay!138
2025-08-13 16:11:30 +00:00
Kelvin e080702a52 Fix dislike color 2025-08-13 17:56:27 +02:00
Kelvin 3909343adc Pre-generate support shorts, subtitle size, short like/dislike color 2025-08-13 00:23:54 +02:00
Kelvin dc76934d0e Add explicit long type for dash dwonload length 2025-08-12 17:05:54 +02:00
Kelvin 6cf47d592a Various shorts improvements, login warnings support, etc 2025-08-12 02:03:04 +02:00
Kai a058bdbfef merge 2025-02-20 16:15:04 -06:00
Kai 3d863b9c89 - fix PiP when player closed
- improve PiP speed on Android 12+
2024-12-17 14:46:18 -06:00
Kai 04c0679930 - fix PiP issues
- fix overlay issues
2024-12-17 13:44:16 -06:00
Kai 45ded8d384 update MotionLayout code
fix touch region bug
move more pieces to xml
upgrade full screen gesture
improve mini player UI
2024-12-16 18:21:57 -06:00
Kai f64efdc964 deps 2024-12-14 14:44:40 -06:00
63 changed files with 1557 additions and 1564 deletions
+3 -3
View File
@@ -154,16 +154,16 @@ android {
}
dependencies {
implementation 'com.google.dagger:dagger:2.48'
//implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
implementation 'com.google.android.material:material:1.12.0'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
//Images
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
@@ -603,6 +603,11 @@ class Settings : FragmentedStorageFileJson() {
else -> 2.0
}
}
@AdvancedField
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
var shortsPregenerate: Boolean = false;
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.others.LoginWebViewClient
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
@@ -74,6 +75,7 @@ class LoginActivity : AppCompatActivity() {
finish();
};
var isFirstLoad = true;
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
webViewClient.onPageLoaded.subscribe { view, url ->
_textUrl.setText(url ?: "");
@@ -86,6 +88,19 @@ class LoginActivity : AppCompatActivity() {
//TODO: Find most reliable way to wait for page js to finish
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
}
if(loginWarnings.size > 0) {
synchronized(loginWarnings) {
val warning = loginWarnings.find { it.url.matches(it.getRegex()) };
if(warning != null) {
if(warning.once == true)
loginWarnings.remove(warning);
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
UIDialogs.Action("Understood", {
}, UIDialogs.ActionStyle.PRIMARY));
}
}
}
}
_webView.settings.domStorageEnabled = true;
@@ -365,14 +365,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragVideoDetail.onMinimize.subscribe {
updateSegmentPaddings();
};
_fragVideoDetail.onTransitioning.subscribe {
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
_fragContainerOverlay.elevation =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
else
_fragContainerOverlay.elevation =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
}
_fragVideoDetail.onCloseEvent.subscribe {
_fragMainHome.setPreviewsEnabled(true);
@@ -1143,8 +1135,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (_fragContainerVideoDetail.visibility != View.VISIBLE)
_fragContainerVideoDetail.visibility = View.VISIBLE;
when (segment.state) {
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail()
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail()
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail(false)
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail(false)
else -> {}
}
segment.onShown(parameter, isBack);
@@ -1269,7 +1261,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
private fun updateSegmentPaddings() {
var paddingBottom = 0f;
if (fragCurrent.hasBottomBar)
@@ -1280,9 +1271,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
.toInt()
);
if (_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP;
_fragContainerMain.setPadding(
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics)
.toInt()
@@ -1,6 +1,10 @@
package com.futo.platformplayer.api.media.platforms.js
@kotlinx.serialization.Serializable
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.util.Dictionary
@Serializable
class SourcePluginAuthConfig(
val loginUrl: String,
val completionUrl: String? = null,
@@ -11,5 +15,26 @@ class SourcePluginAuthConfig(
val userAgent: String? = null,
val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<String>>? = null,
val loginWarning: String? = null
) { }
val loginWarning: String? = null,
val loginWarnings: List<Warning>? = null
) {
@Serializable
class Warning(
val url: String,
val text: String?,
val details: String? = null,
val once: Boolean? = true
) {
@Contextual
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
_regex = reg;
return reg;
}
}
}
}
@@ -17,6 +17,7 @@ import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
@@ -57,12 +58,24 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
hasGenerate = _obj.has("generate");
}
private var _pregenerate: V8Deferred<String?>? = null;
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
_pregenerate = generateAsync(scope);
return _pregenerate;
}
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val pregenerated = _pregenerate;
if(pregenerated != null) {
Logger.w("JSDashManifestRawAudioSource", "Returning pre-generated audio");
return pregenerated;
}
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = null;
@@ -18,6 +18,7 @@ import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -65,11 +66,22 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
hasGenerate = _obj.has("generate");
}
private var _pregenerate: V8Deferred<String?>? = null;
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
_pregenerate = generateAsync(scope);
return _pregenerate;
}
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val pregenerated = _pregenerate;
if(pregenerated != null) {
Logger.w("JSDashManifestRawSource", "Returning pre-generated video");
return pregenerated;
}
val plugin = _plugin.getUnderlyingPlugin();
@@ -12,7 +12,7 @@ class MultiDistributionContentPager<T : IPlatformContent> : MultiPager<T> {
private val dist : HashMap<IPager<T>, Float>;
private val distConsumed : HashMap<IPager<T>, Float>;
constructor(pagers : Map<IPager<T>, Float>) : super(pagers.keys.toMutableList()) {
constructor(pagers : Map<IPager<T>, Float>, pageSize: Int = 9) : super(pagers.keys.toMutableList(), false, pageSize) {
val distTotal = pagers.values.sum();
dist = HashMap();
@@ -719,7 +719,7 @@ class VideoDownload {
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
var written = 0;
var written: Long = 0;
var indexCounter = 0;
onProgress(foundCues.count().toLong(), 0, 0);
for(cue in foundCues) {
@@ -744,7 +744,7 @@ class VideoDownload {
indexCounter++;
}
sourceLength = written.toLong();
sourceLength = written;
Logger.i(TAG, "$name downloadSource Finished");
}
@@ -194,7 +194,11 @@ class PackageBridge : V8Package {
val stackTrace = Thread.currentThread().stackTrace;
val callerMethod = stackTrace.findLast {
it.className == JSClient::class.java.name
it.className == JSClient::class.java.name &&
it.methodName != "isBusy" &&
it.methodName != "busy" &&
it.methodName != "getCopy" &&
it.methodName != "isBusyWith"
}?.methodName ?: "";
val session = StateApp.instance.sessionId;
val pluginId = _plugin.config.id;
@@ -199,7 +199,7 @@ class ChannelFragment : MainFragment() {
when (v) {
is IPlatformVideo -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail(false)
}
is IPlatformPlaylist -> {
@@ -245,7 +245,7 @@ class ChannelFragment : MainFragment() {
when (contentType) {
ContentType.MEDIA -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail(false)
}
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
@@ -197,9 +197,9 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
StatePlayer.instance.insertToQueue(content, true);
} else {
if (Settings.instance.playback.shouldResumePreview(time))
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail();
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail(false);
else
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail(false);
}
} else if (content is IPlatformPlaylist) {
fragment.navigate<RemotePlaylistFragment>(content);
@@ -218,7 +218,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
when(contentType) {
ContentType.MEDIA -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail(false)
}
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url)
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
@@ -174,7 +174,7 @@ class DownloadsFragment : MainFragment() {
.asAnyWithTop(findViewById(R.id.downloads_top)) {
it.onClick.subscribe {
StatePlayer.instance.clearQueue();
_frag.navigate<VideoDetailFragment>(it).maximizeVideoDetail();
_frag.navigate<VideoDetailFragment>(it).maximizeVideoDetail(false);
}
};
@@ -247,7 +247,7 @@ class HistoryFragment : MainFragment() {
val diff = v.video.duration - v.position;
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
StatePlayer.instance.clearQueue();
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail(false);
_editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
@@ -1,46 +1,27 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.text.Spanned
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.SoundEffectConstants
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.view.animation.OvershootInterpolator
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
@@ -54,40 +35,30 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.dp
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.fragment.mainactivity.special.CommentsModalBottomSheet
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanBitrate
import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.MonetizationView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.buttons.ShortsButton
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.DescriptionOverlay
import com.futo.platformplayer.views.overlays.RepliesOverlay
import com.futo.platformplayer.views.overlays.SupportOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
@@ -95,20 +66,17 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTitle
import com.futo.platformplayer.views.pills.OnLikeDislikeUpdatedArgs
import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.platformplayer.views.segments.CommentsList
import com.futo.platformplayer.views.video.FutoShortPlayer
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_AUDIO_CONTAINERS
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_VIDEO_CONTAINERS
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models
import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
//import com.google.android.material.button.MaterialButton
import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -116,30 +84,29 @@ import userpackage.Protocol
@UnstableApi
class ShortView : FrameLayout {
private lateinit var mainFragment: MainFragment
private lateinit var fragment: MainFragment
private val player: FutoShortPlayer
private val channelInfo: LinearLayout
private val creatorThumbnail: CreatorThumbnail
private val channelName: TextView
private val videoTitle: TextView
private val videoSubtitle: TextView
private val platformIndicator: PlatformIndicator
//TODO: Replace with non-material button
private val backButton: MaterialButton
private val backButtonContainer: ConstraintLayout
private val likeContainer: FrameLayout
private val dislikeContainer: FrameLayout
private val likeButton: MaterialButton
private val likeCount: TextView
private val dislikeButton: MaterialButton
private val dislikeCount: TextView
private val likeButton: ShortsButton
//private val likeCount: TextView
private val dislikeButton: ShortsButton
//private val dislikeCount: TextView
private val commentsButton: MaterialButton
private val shareButton: MaterialButton
private val refreshButton: MaterialButton
private val refreshButtonContainer: View
private val qualityButton: MaterialButton
private val commentsButton: ShortsButton
private val shareButton: ShortsButton
private val refreshButton: ShortsButton
private val qualityButton: ShortsButton
private val playPauseOverlay: FrameLayout
private val playPauseIcon: ImageView
@@ -173,18 +140,21 @@ class ShortView : FrameLayout {
private val onLikeDislikeUpdated = Event1<OnLikeDislikeUpdatedArgs>()
private val onVideoUpdated = Event1<IPlatformVideo?>()
//TODO: Replace with non-material UI? Only true dependency on Material left
private val bottomSheet: CommentsModalBottomSheet = CommentsModalBottomSheet()
var likes: Long = 0
set(value) {
field = value
likeCount.text = value.toString()
likeButton.withPrimaryText(value.toString());
//likeCount.text = value.toString()
}
var dislikes: Long = 0
set(value) {
field = value
dislikeCount.text = value.toString()
dislikeButton.withPrimaryText(value.toString());
//dislikeCount.text = value.toString()
}
constructor(inflater: LayoutInflater, fragment: MainFragment, overlayQualityContainer: FrameLayout) : this(inflater.context) {
@@ -194,7 +164,7 @@ class ShortView : FrameLayout {
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
)
this.mainFragment = fragment
this.fragment = fragment
bottomSheet.mainFragment = fragment
}
@@ -217,19 +187,17 @@ class ShortView : FrameLayout {
creatorThumbnail = findViewById(R.id.creator_thumbnail)
channelName = findViewById(R.id.channel_name)
videoTitle = findViewById(R.id.video_title)
videoSubtitle = findViewById(R.id.video_subtitle)
platformIndicator = findViewById(R.id.short_platform_indicator)
backButton = findViewById(R.id.back_button)
backButtonContainer = findViewById(R.id.back_button_container)
likeContainer = findViewById(R.id.like_container)
dislikeContainer = findViewById(R.id.dislike_container)
likeButton = findViewById(R.id.like_button)
likeCount = findViewById(R.id.like_count)
//likeCount = findViewById(R.id.like_count)
dislikeButton = findViewById(R.id.dislike_button)
dislikeCount = findViewById(R.id.dislike_count)
//dislikeCount = findViewById(R.id.dislike_count)
commentsButton = findViewById(R.id.comments_button)
shareButton = findViewById(R.id.share_button)
refreshButton = findViewById(R.id.refresh_button)
refreshButtonContainer = findViewById(R.id.refresh_button_container)
qualityButton = findViewById(R.id.quality_button)
playPauseOverlay = findViewById(R.id.play_pause_overlay)
playPauseIcon = findViewById(R.id.play_pause_icon)
@@ -258,48 +226,44 @@ class ShortView : FrameLayout {
}
onVideoUpdated.subscribe {
Logger.i(TAG, "Shorts videoUpdated [${it?.name}] (isDetail: ${it is IPlatformVideoDetails}, thumbnail: ${it?.author?.thumbnail})");
videoTitle.text = it?.name
videoSubtitle.text = if(it is IPlatformVideoDetails) it?.description; else "";
platformIndicator.setPlatformFromClientID(it?.id?.pluginId)
creatorThumbnail.setThumbnail(it?.author?.thumbnail, true)
channelName.text = it?.author?.name
}
backButton.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
mainFragment.closeSegment()
fragment.closeSegment()
}
channelInfo.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
mainFragment.navigate<ChannelFragment>(video?.author)
fragment.navigate<ChannelFragment>(video?.author)
}
videoTitle.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
if (!bottomSheet.isAdded) {
bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG)
bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
}
}
commentsButton.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
commentsButton.onClick.subscribe {
if (!bottomSheet.isAdded) {
bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG)
bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
}
}
shareButton.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
shareButton.onClick.subscribe {
val url = video?.shareUrl ?: video?.url
mainFragment.startActivity(Intent.createChooser(Intent().apply {
fragment.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, url)
type = "text/plain"
}, null))
}
refreshButton.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
refreshButton.onClick.subscribe {
onResetTriggered.emit()
}
@@ -308,14 +272,12 @@ class ShortView : FrameLayout {
false
}
qualityButton.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
qualityButton.onClick.subscribe {
showVideoSettings()
}
likeButton.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
val checked = !likeButton.isChecked
likeButton.onClick.subscribe {
val checked = likeButton.iconId == R.drawable.ic_thumb_up_s // !likeButton.isChecked
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
if (checked) {
likes++
@@ -323,24 +285,27 @@ class ShortView : FrameLayout {
likes--
}
likeButton.isChecked = checked
if(checked)
likeButton.withIcon(R.drawable.ic_thumb_up_s_filled) //.isChecked = checked
else
likeButton.withIcon(R.drawable.ic_thumb_up_s)
if (dislikeButton.isChecked && checked) {
dislikeButton.isChecked = false
if (dislikeButton.iconId == R.drawable.ic_thumb_down_s_filled && checked) {
//dislikeButton.isChecked = false
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
dislikes--
}
onLikeDislikeUpdated.emit(
OnLikeDislikeUpdatedArgs(
it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked
it, likes, checked, dislikes, !checked
)
)
}
}
dislikeButton.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
val checked = !dislikeButton.isChecked
dislikeButton.onClick.subscribe {
val checked = dislikeButton.iconId == R.drawable.ic_thumb_down_s //!dislikeButton.isChecked
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
if (checked) {
dislikes++
@@ -348,16 +313,21 @@ class ShortView : FrameLayout {
dislikes--
}
dislikeButton.isChecked = checked
//dislikeButton.isChecked = checked
if(checked)
dislikeButton.withIcon(R.drawable.ic_thumb_down_s_filled) //.isChecked = checked
else
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
if (likeButton.isChecked && checked) {
likeButton.isChecked = false
if (likeButton.iconId == R.drawable.ic_thumb_up_s_filled && checked) {
//likeButton.isChecked = false
likeButton.withIcon(R.drawable.ic_thumb_up_s);
likes--
}
onLikeDislikeUpdated.emit(
OnLikeDislikeUpdatedArgs(
it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked
it, likes, !checked, dislikes, checked
)
)
}
@@ -366,11 +336,11 @@ class ShortView : FrameLayout {
onLikesLoaded.subscribe(tag) { rating, liked, disliked ->
likes = rating.likes
dislikes = rating.dislikes
likeButton.isChecked = liked
dislikeButton.isChecked = disliked
//likeButton.isChecked = liked
//dislikeButton.isChecked = disliked
dislikeContainer.visibility = VISIBLE
likeContainer.visibility = VISIBLE
dislikeButton.visibility = VISIBLE
likeButton.visibility = VISIBLE
}
player.onPlaybackStateChanged.subscribe {
@@ -565,7 +535,7 @@ class ShortView : FrameLayout {
var toSet: ISubtitleSource? = subtitleSource
if (_lastSubtitleSource == subtitleSource) toSet = null
mainFragment.lifecycleScope.launch(Dispatchers.Main) {
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
player.swapSubtitles(toSet)
} catch (e: Throwable) {
@@ -625,7 +595,7 @@ class ShortView : FrameLayout {
@Suppress("unused")
fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) {
this.mainFragment = fragment
this.fragment = fragment
this.bottomSheet.mainFragment = fragment
this.overlayQualityContainer = overlayQualityContainer
}
@@ -636,10 +606,10 @@ class ShortView : FrameLayout {
}
this.video = video
refreshButtonContainer.visibility = if (isChannelShortsMode) {
refreshButton.visibility = if (isChannelShortsMode) {
GONE
} else {
VISIBLE
GONE //TODO: Revert?
}
backButtonContainer.visibility = if (isChannelShortsMode) {
VISIBLE
@@ -695,8 +665,8 @@ class ShortView : FrameLayout {
}
private fun loadLikes(video: IPlatformVideo) {
likeContainer.visibility = GONE
dislikeContainer.visibility = GONE
likeButton.visibility = GONE
dislikeButton.visibility = GONE
loadLikesTask?.cancel()
loadLikesTask =
@@ -735,13 +705,13 @@ class ShortView : FrameLayout {
args.processHandle.opinion(ref, Opinion.neutral)
}
mainFragment.lifecycleScope.launch(Dispatchers.IO) {
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
Logger.i(CommentsModalBottomSheet.TAG, "Started backfill")
Logger.i(TAG, "Started backfill")
args.processHandle.fullyBackfillServersAnnounceExceptions()
Logger.i(CommentsModalBottomSheet.TAG, "Finished backfill")
Logger.i(TAG, "Finished backfill")
} catch (e: Throwable) {
Logger.e(CommentsModalBottomSheet.TAG, "Failed to backfill servers", e)
Logger.e(TAG, "Failed to backfill servers", e)
}
}
@@ -763,20 +733,41 @@ class ShortView : FrameLayout {
setLoading(true)
Logger.i(TAG, "Shorts loadVideo [${url}]");
val timeLoadVideoStart = System.currentTimeMillis();
loadVideoTask = TaskHandler<String, IPlatformVideoDetails>(
StateApp.instance.scopeGetter, {
val result = StatePlatform.instance.getContentDetails(it).await()
if (result !is IPlatformVideoDetails) throw IllegalStateException("Expected media content, found ${result.contentType}")
return@TaskHandler result
}).success { result ->
videoDetails = result
video = result
val timeLoadVideo = System.currentTimeMillis() - timeLoadVideoStart;
Logger.i(TAG, "Shorts loadVideo [${url}] took ${timeLoadVideo}ms");
videoDetails = result
video = result
bottomSheet.video = result
if(Settings.instance.playback.shortsPregenerate)
fragment.lifecycleScope.launch(Dispatchers.IO) {
if(result != null) {
val prefVid = VideoHelper.selectBestVideoSource(result.video, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
val prefAud = VideoHelper.selectBestAudioSource(result.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
setLoading(false)
if(prefVid != null && prefVid is JSDashManifestRawSource) {
Logger.i(TAG, "Shorts pregenerating video (${result.name})");
prefVid.pregenerateAsync(fragment.lifecycleScope);
}
if(prefAud != null && prefAud is JSDashManifestRawAudioSource) {
Logger.i(TAG, "Shorts pregenerating audio (${result.name})");
prefAud.pregenerateAsync(fragment.lifecycleScope);
}
}
}
if (playWhenReady) playVideo()
bottomSheet.video = result
setLoading(false)
if (playWhenReady) playVideo()
}.exception<NoPlatformClientException> {
Logger.w(TAG, "exception<NoPlatformClientException>", it)
UIDialogs.showDialog(
@@ -799,7 +790,7 @@ class ShortView : FrameLayout {
UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${it.availableWhen}.", "Close") { }
}.exception<ScriptImplementationException> {
Logger.w(TAG, "exception<ScriptImplementationException>", it)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, { loadVideo(url) }, null, mainFragment)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, { loadVideo(url) }, null, fragment)
}.exception<ScriptAgeException> {
Logger.w(TAG, "exception<ScriptAgeException>", it)
UIDialogs.showDialog(
@@ -812,10 +803,10 @@ class ShortView : FrameLayout {
)
}.exception<ScriptException> {
Logger.w(TAG, "exception<ScriptException>", it)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, { loadVideo(url) }, null, mainFragment)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, { loadVideo(url) }, null, fragment)
}.exception<Throwable> {
Logger.w(ChannelFragment.TAG, "Failed to load video.", it)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, mainFragment)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, fragment)
}
loadVideoTask?.run(url)
@@ -849,6 +840,7 @@ class ShortView : FrameLayout {
}
val thumbnail = videoDetails.thumbnails.getHQThumbnail()
/*
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
.load(thumbnail).into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
@@ -860,8 +852,9 @@ class ShortView : FrameLayout {
}
})
else player.setArtwork(null)
*/
mainFragment.lifecycleScope.launch(Dispatchers.Main) {
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
if (subtitleSource != null) player.swapSubtitles(subtitleSource)
@@ -887,397 +880,4 @@ class ShortView : FrameLayout {
const val TAG = "VideoDetailView"
}
class CommentsModalBottomSheet : BottomSheetDialogFragment() {
var mainFragment: MainFragment? = null
private lateinit var containerContent: FrameLayout
private lateinit var containerContentMain: LinearLayout
private lateinit var containerContentReplies: RepliesOverlay
private lateinit var containerContentDescription: DescriptionOverlay
private lateinit var containerContentSupport: SupportOverlay
private lateinit var title: TextView
private lateinit var subTitle: TextView
private lateinit var channelName: TextView
private lateinit var channelMeta: TextView
private lateinit var creatorThumbnail: CreatorThumbnail
private lateinit var channelButton: LinearLayout
private lateinit var monetization: MonetizationView
private lateinit var platform: PlatformIndicator
private lateinit var textLikes: TextView
private lateinit var textDislikes: TextView
private lateinit var layoutRating: LinearLayout
private lateinit var imageDislikeIcon: ImageView
private lateinit var imageLikeIcon: ImageView
private lateinit var description: TextView
private lateinit var descriptionContainer: LinearLayout
private lateinit var descriptionViewMore: TextView
private lateinit var commentsList: CommentsList
private lateinit var addCommentView: AddCommentView
private var polycentricProfile: PolycentricProfile? = null
private lateinit var buttonPolycentric: Button
private lateinit var buttonPlatform: Button
private var tabIndex: Int? = null
private var contentOverlayView: View? = null
lateinit var video: IPlatformVideoDetails
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
private val _taskLoadPolycentricProfile =
TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }).success { setPolycentricProfile(it, animate = true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it)
}
override fun onCreateDialog(
savedInstanceState: Bundle?,
): Dialog {
val bottomSheetDialog =
BottomSheetDialog(requireContext(), R.style.Custom_BottomSheetDialog_Theme)
bottomSheetDialog.setContentView(R.layout.modal_comments)
behavior = bottomSheetDialog.behavior
// TODO figure out how to not need all of these non null assertions
containerContent = bottomSheetDialog.findViewById(R.id.content_container)!!
containerContentMain = bottomSheetDialog.findViewById(R.id.videodetail_container_main)!!
containerContentReplies =
bottomSheetDialog.findViewById(R.id.videodetail_container_replies)!!
containerContentDescription =
bottomSheetDialog.findViewById(R.id.videodetail_container_description)!!
containerContentSupport =
bottomSheetDialog.findViewById(R.id.videodetail_container_support)!!
title = bottomSheetDialog.findViewById(R.id.videodetail_title)!!
subTitle = bottomSheetDialog.findViewById(R.id.videodetail_meta)!!
channelName = bottomSheetDialog.findViewById(R.id.videodetail_channel_name)!!
channelMeta = bottomSheetDialog.findViewById(R.id.videodetail_channel_meta)!!
creatorThumbnail = bottomSheetDialog.findViewById(R.id.creator_thumbnail)!!
channelButton = bottomSheetDialog.findViewById(R.id.videodetail_channel_button)!!
monetization = bottomSheetDialog.findViewById(R.id.monetization)!!
platform = bottomSheetDialog.findViewById(R.id.videodetail_platform)!!
layoutRating = bottomSheetDialog.findViewById(R.id.layout_rating)!!
textDislikes = bottomSheetDialog.findViewById(R.id.text_dislikes)!!
textLikes = bottomSheetDialog.findViewById(R.id.text_likes)!!
imageLikeIcon = bottomSheetDialog.findViewById(R.id.image_like_icon)!!
imageDislikeIcon = bottomSheetDialog.findViewById(R.id.image_dislike_icon)!!
description = bottomSheetDialog.findViewById(R.id.videodetail_description)!!
descriptionContainer =
bottomSheetDialog.findViewById(R.id.videodetail_description_container)!!
descriptionViewMore =
bottomSheetDialog.findViewById(R.id.videodetail_description_view_more)!!
addCommentView = bottomSheetDialog.findViewById(R.id.add_comment_view)!!
commentsList = bottomSheetDialog.findViewById(R.id.comments_list)!!
buttonPolycentric = bottomSheetDialog.findViewById(R.id.button_polycentric)!!
buttonPlatform = bottomSheetDialog.findViewById(R.id.button_platform)!!
commentsList.onAuthorClick.subscribe { c ->
if (c !is PolycentricPlatformComment) {
return@subscribe
}
val id = c.author.id.value
Logger.i(TAG, "onAuthorClick: $id")
if (id != null && id.startsWith("polycentric://")) {
val navUrl = "https://harbor.social/" + id.substring("polycentric://".length)
mainFragment!!.startActivity(Intent(Intent.ACTION_VIEW, navUrl.toUri()))
}
}
commentsList.onRepliesClick.subscribe { c ->
val replyCount = c.replyCount ?: 0
var metadata = ""
if (replyCount > 0) {
metadata += "$replyCount " + requireContext().getString(R.string.replies)
}
if (c is PolycentricPlatformComment) {
var parentComment: PolycentricPlatformComment = c
containerContentReplies.load(tabIndex!! != 0, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, {
val newComment = parentComment.cloneWithUpdatedReplyCount(
(parentComment.replyCount ?: 0) + 1
)
commentsList.replaceComment(parentComment, newComment)
parentComment = newComment
})
} else {
containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) })
}
animateOpenOverlayView(containerContentReplies)
}
if (StatePolycentric.instance.enabled) {
buttonPolycentric.setOnClickListener {
setTabIndex(0)
StateMeta.instance.setLastCommentSection(0)
}
} else {
buttonPolycentric.visibility = GONE
}
buttonPlatform.setOnClickListener {
setTabIndex(1)
StateMeta.instance.setLastCommentSection(1)
}
val ref = Models.referenceFromBuffer(video.url.toByteArray())
addCommentView.setContext(video.url, ref)
if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) {
setTabIndex(2, true)
} else {
when (Settings.instance.comments.defaultCommentSection) {
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
1 -> setTabIndex(1, true)
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
}
}
containerContentDescription.onClose.subscribe { animateCloseOverlayView() }
containerContentReplies.onClose.subscribe { animateCloseOverlayView() }
descriptionViewMore.setOnClickListener {
animateOpenOverlayView(containerContentDescription)
}
updateDescriptionUI(video.description.fixHtmlLinks())
val dp5 =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics)
val dp2 =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics)
//UI
title.text = video.name
channelName.text = video.author.name
if (video.author.subscribers != null) {
channelMeta.text = if ((video.author.subscribers
?: 0) > 0
) video.author.subscribers!!.toHumanNumber() + " " + requireContext().getString(R.string.subscribers) else ""
(channelName.layoutParams as MarginLayoutParams).setMargins(
0, (dp5 * -1).toInt(), 0, 0
)
} else {
channelMeta.text = ""
(channelName.layoutParams as MarginLayoutParams).setMargins(0, (dp2).toInt(), 0, 0)
}
video.author.let {
if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl)
else monetization.setPlatformMembership(null, null)
}
val subTitleSegments: ArrayList<String> = ArrayList()
if (video.viewCount > 0) subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if (video.isLive) requireContext().getString(R.string.watching_now) else requireContext().getString(R.string.views)}")
if (video.datetime != null) {
val diff = video.datetime?.getNowDiffSeconds() ?: 0
val ago = video.datetime?.toHumanNowDiffString(true)
if (diff >= 0) subTitleSegments.add("$ago ago")
else subTitleSegments.add("available in $ago")
}
platform.setPlatformFromClientID(video.id.pluginId)
subTitle.text = subTitleSegments.joinToString("")
creatorThumbnail.setThumbnail(video.author.thumbnail, false)
setPolycentricProfile(null, animate = false)
_taskLoadPolycentricProfile.run(video.author.id)
when (video.rating) {
is RatingLikeDislikes -> {
val r = video.rating as RatingLikeDislikes
layoutRating.visibility = VISIBLE
textLikes.visibility = VISIBLE
imageLikeIcon.visibility = VISIBLE
textLikes.text = r.likes.toHumanNumber()
imageDislikeIcon.visibility = VISIBLE
textDislikes.visibility = VISIBLE
textDislikes.text = r.dislikes.toHumanNumber()
}
is RatingLikes -> {
val r = video.rating as RatingLikes
layoutRating.visibility = VISIBLE
textLikes.visibility = VISIBLE
imageLikeIcon.visibility = VISIBLE
textLikes.text = r.likes.toHumanNumber()
imageDislikeIcon.visibility = GONE
textDislikes.visibility = GONE
}
else -> {
layoutRating.visibility = GONE
}
}
monetization.onSupportTap.subscribe {
containerContentSupport.setPolycentricProfile(polycentricProfile)
animateOpenOverlayView(containerContentSupport)
}
monetization.onStoreTap.subscribe {
polycentricProfile?.systemState?.store?.let {
try {
val uri = it.toUri()
val intent = Intent(Intent.ACTION_VIEW)
intent.data = uri
requireContext().startActivity(intent)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to open URI: '${it}'.", e)
}
}
}
monetization.onUrlTap.subscribe {
mainFragment!!.navigate<BrowserFragment>(it)
}
addCommentView.onCommentAdded.subscribe {
commentsList.addComment(it)
}
channelButton.setOnClickListener {
mainFragment!!.navigate<ChannelFragment>(video.author)
}
return bottomSheetDialog
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
animateCloseOverlayView()
}
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
polycentricProfile = profile
val dp35 = 35.dp(requireContext().resources)
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
if (avatar != null) {
creatorThumbnail.setThumbnail(avatar, animate)
} else {
creatorThumbnail.setThumbnail(video.author.thumbnail, animate)
creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto())
}
val username = profile?.systemState?.username
if (username != null) {
channelName.text = username
}
monetization.setPolycentricProfile(profile)
}
private fun setTabIndex(index: Int?, forceReload: Boolean = false) {
Logger.i(TAG, "setTabIndex (index: ${index}, forceReload: ${forceReload})")
val changed = tabIndex != index || forceReload
if (!changed) {
return
}
tabIndex = index
buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac, null))
buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac, null))
when (index) {
null -> {
addCommentView.visibility = GONE
commentsList.clear()
}
0 -> {
addCommentView.visibility = VISIBLE
fetchPolycentricComments()
}
1 -> {
addCommentView.visibility = GONE
fetchComments()
}
}
}
private fun fetchComments() {
Logger.i(TAG, "fetchComments")
video.let {
commentsList.load(true) { StatePlatform.instance.getComments(it) }
}
}
private fun fetchPolycentricComments() {
Logger.i(TAG, "fetchPolycentricComments")
val video = video
val idValue = video.id.value
if (video.url.isEmpty()) {
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
commentsList.clear()
return
}
val ref = Models.referenceFromBuffer(video.url.toByteArray())
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); }
}
private fun updateDescriptionUI(text: Spanned) {
containerContentDescription.load(text)
description.text = text
if (description.text.isNotEmpty()) descriptionContainer.visibility = VISIBLE
else descriptionContainer.visibility = GONE
}
private fun animateOpenOverlayView(view: View) {
if (contentOverlayView != null) {
Logger.e(TAG, "Content overlay already open")
return
}
behavior.isDraggable = false
behavior.state = BottomSheetBehavior.STATE_EXPANDED
val animHeight = containerContentMain.height
view.translationY = animHeight.toFloat()
view.visibility = VISIBLE
view.animate().setDuration(300).translationY(0f).withEndAction {
contentOverlayView = view
}.start()
}
private fun animateCloseOverlayView() {
val curView = contentOverlayView
if (curView == null) {
Logger.e(TAG, "No content overlay open")
return
}
behavior.isDraggable = true
val animHeight = contentOverlayView!!.height
curView.animate().setDuration(300).translationY(animHeight.toFloat()).withEndAction {
curView.visibility = GONE
contentOverlayView = null
}.start()
}
companion object {
const val TAG = "ModalBottomSheet"
}
}
}
@@ -11,6 +11,7 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.annotation.OptIn
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
@@ -25,6 +26,9 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.system.measureTimeMillis
@UnstableApi
class ShortsFragment : MainFragment() {
@@ -35,6 +39,7 @@ class ShortsFragment : MainFragment() {
private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null
private var nextPageTask: TaskHandler<ShortsFragment, List<IPlatformVideo>>? = null
//TODO: Reduce number of pagers (1, or at most 2)
private var mainShortsPager: IPager<IPlatformVideo>? = null
private val mainShorts: MutableList<IPlatformVideo> = mutableListOf()
@@ -58,6 +63,7 @@ class ShortsFragment : MainFragment() {
private var customViewAdapter: CustomViewAdapter? = null
// we just completely reset the data structure so we want to tell the adapter that
//TODO: Move most of this logic to ShortsView
@SuppressLint("NotifyDataSetChanged")
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
(activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
@@ -118,7 +124,6 @@ class ShortsFragment : MainFragment() {
overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview)
sourcesButton.onClick.subscribe {
sourcesButton.playSoundEffect(SoundEffectConstants.CLICK)
navigate<SourcesFragment>()
}
@@ -145,7 +150,7 @@ class ShortsFragment : MainFragment() {
this.customViewAdapter = customViewAdapter
if (loadPagerTask == null && currentShorts.isEmpty()) {
if (loadPagerTask == null) {// && currentShorts.isEmpty()) {
loadPager()
loadPagerTask!!.success {
@@ -207,28 +212,29 @@ class ShortsFragment : MainFragment() {
}
private fun nextPage() {
nextPageTask?.cancel()
val nextPageTask =
TaskHandler<ShortsFragment, List<IPlatformVideo>>(StateApp.instance.scopeGetter, {
currentShortsPager!!.nextPage()
return@TaskHandler currentShortsPager!!.getResults()
}).success { newVideos ->
Logger.i(TAG, "ShortsFragment nextPage");
lifecycleScope.launch(Dispatchers.IO) {
try {
val time = measureTimeMillis {
currentShortsPager!!.nextPage();
}
val newVideos = currentShortsPager!!.getResults();
val prevCount = customViewAdapter!!.itemCount
Logger.i(TAG, "Shorts nextPage took ${time}ms, ${prevCount}-${prevCount + newVideos.size}, hasMore: ${currentShortsPager?.hasMorePages()}");
currentShorts.addAll(newVideos)
if (isChannelShortsMode) {
channelShorts.addAll(newVideos)
} else {
mainShorts.addAll(newVideos)
}
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
lifecycleScope.launch(Dispatchers.Main) {
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
}
nextPageTask = null
} catch (ex: Throwable) {
Logger.e(TAG, "Shorts Failed to call nextPage", ex);
}
nextPageTask.run(this)
this.nextPageTask = nextPageTask
}
}
// we just completely reset the data structure so we want to tell the adapter that
@@ -236,12 +242,16 @@ class ShortsFragment : MainFragment() {
private fun loadPager() {
loadPagerTask?.cancel()
Logger.i(TAG, "Shorts loadPage");
var loadPageStart = System.currentTimeMillis();
val loadPagerTask =
TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, {
val pager = StatePlatform.instance.getShorts()
val pager = StatePlatform.instance.getShorts();
return@TaskHandler pager
}).success { pager ->
val timeLoadPage = System.currentTimeMillis() - loadPageStart;
Logger.i(TAG, "Shorts loadPage took ${timeLoadPage}ms");
mainShorts.clear()
mainShorts.addAll(pager.getResults())
mainShortsPager = pager
@@ -259,7 +269,7 @@ class ShortsFragment : MainFragment() {
loadPagerTask = null
}.exception<Throwable> { err ->
val message = "Unable to load shorts $err"
Logger.i(TAG, message)
Logger.w(TAG, message, err)
if (context != null) {
UIDialogs.showDialog(
requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action(
@@ -329,6 +339,7 @@ class ShortsFragment : MainFragment() {
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
Logger.i(TAG, "Shorts change (position: ${position}): ${videos[position].name} (${videos[position].id.value})")
holder.shortView.changeVideo(videos[position], isChannelShortsMode())
if (position == itemCount - 1) {
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.views.buttons.BigButton
@@ -152,11 +153,50 @@ class SourceDetailFragment : MainFragment() {
if(field is View)
field.isVisible = false;
}
if(!source.capabilities.hasGetUserHistory) {
if(!source.capabilities.hasGetUserHistory || !source.isLoggedIn) {
val field = _settingsAppForm.findField("sync");
if(field is View)
field.isVisible = false;
}
else {
val field = _settingsAppForm.findField("syncHistory");
field?.onChanged?.subscribe { field, new, old ->
if(old != new && new == true && StatePlatform.instance.isClientEnabled(config.id)) {
UIDialogs.showDialog(context, R.drawable.ic_sources, "Would you like to sync now?",
"This will attempt to update your history from the platform, when this setting is enabled, it is done during startup.", null, 0,
UIDialogs.Action("No", {
}),
UIDialogs.Action("Yes", {
UIDialogs.showDialogProgress(context, {
it.setText("Importing history..");
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val client = StatePlatform.instance.getClient(config.id);
if (client != null && client is JSClient) {
val count = StateHistory.instance.syncRemoteHistory(client);
withContext(Dispatchers.Main) {
it.hide();
if(count > 0)
UIDialogs.showDialogOk(context, R.drawable.ic_pair_success, "Imported ${count} history items");
else
UIDialogs.showDialogOk(context, R.drawable.ic_help, "Imported no history items");
}
}
}
catch(ex: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.appToast("Sync History failed due to:\n" + ex.message);
it.hide();
}
}
}
});
}, UIDialogs.ActionStyle.PRIMARY));
}
}
}
_settingsAppForm.onChanged.clear();
_settingsAppForm.onChanged.subscribe { field, value ->
_settingsAppChanged = true;
@@ -34,13 +34,12 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
import com.futo.platformplayer.views.containers.CustomMotionLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
//region Fragment
@UnstableApi
class VideoDetailFragment() : MainFragment() {
@@ -51,8 +50,8 @@ class VideoDetailFragment() : MainFragment() {
private var _isActive: Boolean = false;
private var _viewDetail : VideoDetailView? = null;
private var _view : SingleViewTouchableMotionLayout? = null;
private var _viewDetail : VideoDetailView? = null
private var _motionLayout: CustomMotionLayout? = null
var isFullscreen : Boolean = false;
/**
@@ -61,8 +60,6 @@ class VideoDetailFragment() : MainFragment() {
*/
var isMinimizingFromFullScreen : Boolean = false;
val onFullscreenChanged = Event1<Boolean>();
var isTransitioning : Boolean = false
private set;
var isInPictureInPicture : Boolean = false
private set;
@@ -78,13 +75,8 @@ class VideoDetailFragment() : MainFragment() {
val currentUrl get() = _viewDetail?.currentUrl;
val onMinimize = Event0();
val onTransitioning = Event1<Boolean>();
val onMaximized = Event0();
private var _isInitialMaximize = true;
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
private var _leavingPiP = false;
@@ -295,22 +287,17 @@ class VideoDetailFragment() : MainFragment() {
fun minimizeVideoDetail() {
_viewDetail?.setFullscreen(false);
if(_view != null)
_view!!.transitionToStart();
_motionLayout?.transitionToState(R.id.collapsed)
}
fun maximizeVideoDetail(instant: Boolean = false) {
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
state = State.MAXIMIZED;
onMaximized.emit();
fun maximizeVideoDetail(instant: Boolean) {
state = State.MAXIMIZED
onMaximized.emit()
if(instant) {
_motionLayout?.setTransition(R.id.maximize)
_motionLayout?.progress = 1f
} else {
_motionLayout?.transitionToState(R.id.expanded)
}
_view?.let {
if(!instant) {
it.transitionToEnd();
} else {
it.progress = 1f;
onTransitioning.emit(true);
}
};
}
fun closeVideoDetails() {
Logger.i(TAG, "closeVideoDetails()")
@@ -323,83 +310,48 @@ class VideoDetailFragment() : MainFragment() {
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_view = inflater.inflate(R.layout.fragment_video_detail, container, false) as SingleViewTouchableMotionLayout;
_viewDetail = _view!!.findViewById<VideoDetailView>(R.id.fragview_videodetail).also {
it.applyFragment(this);
it.onFullscreenChanged.subscribe(::onFullscreenChanged);
it.onVideoChanged.subscribe(::onVideoChanged)
it.onMinimize.subscribe {
isMinimizingFromFullScreen = true
_view!!.transitionToStart();
};
it.onClose.subscribe {
Logger.i(TAG, "onClose")
closeVideoDetails();
};
it.onMaximize.subscribe { maximizeVideoDetail(it) };
it.onEnterPictureInPicture.subscribe {
Logger.i(TAG, "onEnterPictureInPicture")
isInPictureInPicture = true;
_viewDetail?.handleEnterPictureInPicture();
_viewDetail?.invalidate();
};
it.onTouchCancel.subscribe {
val v = _view ?: return@subscribe;
if (v.progress >= 0.5 && v.progress < 1) {
maximizeVideoDetail();
}
if (v.progress < 0.5 && v.progress > 0) {
minimizeVideoDetail();
}
};
}
_view!!.setTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {
val viewDetail = VideoDetailView(this, inflater);
_motionLayout = viewDetail.findViewById(R.id.videodetail_root)
viewDetail.applyFragment(this);
viewDetail.onFullscreenChanged.subscribe(::onFullscreenChanged);
viewDetail.onVideoChanged.subscribe(::onVideoChanged)
viewDetail.onMinimize.subscribe {
isMinimizingFromFullScreen = true
_motionLayout?.transitionToState(R.id.collapsed)
};
viewDetail.onClose.subscribe {
Logger.i(TAG, "onClose")
closeVideoDetails();
};
viewDetail.onMaximize.subscribe { maximizeVideoDetail(it) };
viewDetail.onEnterPictureInPicture.subscribe {
Logger.i(TAG, "onEnterPictureInPicture")
isInPictureInPicture = true;
_viewDetail?.handleEnterPictureInPicture();
_viewDetail?.invalidate();
};
_motionLayout!!.addTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
_viewDetail?.stopAllGestures()
if (state != State.MINIMIZED && progress < 0.1) {
state = State.MINIMIZED;
if (state != State.MINIMIZED && currentId == R.id.collapsed) {
state = State.MINIMIZED
isMinimizingFromFullScreen = false
onMinimize.emit();
}
else if (state != State.MAXIMIZED && progress > 0.9) {
if (_isInitialMaximize) {
state = State.CLOSED;
_isInitialMaximize = false;
}
else {
state = State.MAXIMIZED;
onMaximized.emit();
}
onMinimize.emit()
}
if (isTransitioning && (progress > 0.95 || progress < 0.05)) {
isTransitioning = false;
onTransitioning.emit(isTransitioning);
if(isInPictureInPicture) leavePictureInPictureMode(false); //Workaround to prevent getting stuck in p2p
}
else if (!isTransitioning && (progress < 0.95 && progress > 0.05)) {
isTransitioning = true;
onTransitioning.emit(isTransitioning);
if(isInPictureInPicture) leavePictureInPictureMode(false); //Workaround to prevent getting stuck in p2p
if (state != State.MAXIMIZED && currentId == R.id.expanded) {
state = State.MAXIMIZED
onMaximized.emit()
}
}
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) { }
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) { }
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
});
_view?.let {
if (it.progress >= 0.5 && it.progress < 1.0)
maximizeVideoDetail();
if (it.progress < 0.5 && it.progress > 0.0)
minimizeVideoDetail();
}
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {}
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {}
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
})
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
maximizeVideoDetail();
maximizeVideoDetail(false);
SettingsActivity.settingsActivityClosed.subscribe(this) {
updateOrientation()
@@ -432,12 +384,13 @@ class VideoDetailFragment() : MainFragment() {
}
_autoRotateObserver?.startObserving()
return _view!!;
_viewDetail = viewDetail
return viewDetail
}
fun onUserLeaveHint() {
val viewDetail = _viewDetail;
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.allowBackground}");
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.isAudioOnlyUserAction}");
if (viewDetail === null) {
return
@@ -446,7 +399,7 @@ class VideoDetailFragment() : MainFragment() {
if (viewDetail.shouldEnterPictureInPicture) {
_leavingPiP = false
}
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.allowBackground) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.isAudioOnlyUserAction) {
val params = _viewDetail?.getPictureInPictureParams();
if(params != null) {
Logger.i(TAG, "enterPictureInPictureMode")
@@ -526,7 +479,7 @@ class VideoDetailFragment() : MainFragment() {
private fun stopIfRequired() {
var shouldStop = true;
if (_viewDetail?.allowBackground == true) {
if (_viewDetail?.isAudioOnlyUserAction == true) {
shouldStop = false;
} else if (Settings.instance.playback.isBackgroundPictureInPicture() && !_leavingPiP) {
shouldStop = false;
@@ -554,21 +507,12 @@ class VideoDetailFragment() : MainFragment() {
_portraitOrientationListener?.disableListener()
_autoRotateObserver?.stopObserving()
_viewDetail?.let {
_viewDetail = null;
it.onDestroy();
}
_view = null;
_viewDetail = null;
}
override fun onDestroy() {
super.onDestroy()
_viewDetail?.let {
_viewDetail = null;
it.onDestroy();
}
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
Logger.i(TAG, "onDestroy");
@@ -626,7 +570,7 @@ class VideoDetailFragment() : MainFragment() {
}
updateOrientation();
_view?.allowMotion = !fullscreen;
_motionLayout?.isInteractionEnabled = !fullscreen
}
companion object {
@@ -10,7 +10,6 @@ import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Rect
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
@@ -19,11 +18,10 @@ import android.net.Uri
import android.os.Build
import android.support.v4.media.session.PlaybackStateCompat
import android.text.Spanned
import android.util.AttributeSet
import android.util.Log
import android.util.Rational
import android.util.TypedValue
import android.view.MotionEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
@@ -34,6 +32,7 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
@@ -51,7 +50,6 @@ import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SyncShowPairingCodeActivity.Companion.activity
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.LiveChatManager
import com.futo.platformplayer.api.media.PlatformID
@@ -82,7 +80,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.structures.IPager
@@ -137,9 +134,9 @@ import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.MonetizationView
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoView
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
import com.futo.platformplayer.views.casting.CastView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.containers.CustomMotionLayout
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.ChaptersOverlay
import com.futo.platformplayer.views.overlays.DescriptionOverlay
@@ -170,6 +167,7 @@ import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.protobuf.ByteString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@@ -185,7 +183,7 @@ import kotlin.math.abs
import kotlin.math.roundToLong
@UnstableApi
class VideoDetailView : ConstraintLayout {
class VideoDetailView(fragment: VideoDetailFragment, inflater: LayoutInflater) : FrameLayout(inflater.context) {
private val TAG = "VideoDetailView"
lateinit var fragment: VideoDetailFragment;
@@ -214,7 +212,7 @@ class VideoDetailView : ConstraintLayout {
private val _timeBar: TimeBar;
private var _upNext: UpNextView;
private val rootView: ConstraintLayout;
private val rootView: CustomMotionLayout;
private val _title: TextView;
private val _subTitle: TextView;
@@ -243,7 +241,6 @@ class VideoDetailView : ConstraintLayout {
private val _commentsList: CommentsList;
private var _minimizeProgress: Float = 0f;
private val _buttonSubscribe: SubscribeButton;
private val _buttonPins: RoundButtonGroup;
@@ -265,7 +262,7 @@ class VideoDetailView : ConstraintLayout {
private val _textResume: TextView;
private val _layoutResume: LinearLayout;
private var _jobHideResume: Job? = null;
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
private val _layoutPlayerContainer: FrameLayout;
private val _layoutChangeBottomSection: LinearLayout;
//Overlays
@@ -326,7 +323,7 @@ class VideoDetailView : ConstraintLayout {
val onEnterPictureInPicture = Event0();
val onVideoChanged = Event2<Int, Int>()
var allowBackground: Boolean = false
var isAudioOnlyUserAction: Boolean = false
private set(value) {
if (field != value) {
field = value
@@ -338,12 +335,11 @@ class VideoDetailView : ConstraintLayout {
get() = !preventPictureInPicture &&
!StateCasting.instance.isCasting &&
Settings.instance.playback.isBackgroundPictureInPicture() &&
!allowBackground &&
!isAudioOnlyUserAction &&
isPlaying
val onShouldEnterPictureInPictureChanged = Event0();
val onTouchCancel = Event0();
private var _lastPositionSaveTime: Long = -1;
private val DP_5 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
@@ -361,9 +357,8 @@ class VideoDetailView : ConstraintLayout {
Pair(0, 10) //around live, try every 10 seconds
);
@androidx.annotation.OptIn(UnstableApi::class)
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.fragview_video_detail, this);
init {
inflater.inflate(R.layout.fragview_video_detail, this)
//Declare Views
rootView = findViewById(R.id.videodetail_root);
@@ -416,8 +411,7 @@ class VideoDetailView : ConstraintLayout {
_textSkip = findViewById(R.id.text_skip);
_layoutResume = findViewById(R.id.layout_resume);
_textResume = findViewById(R.id.text_resume);
_layoutPlayerContainer = findViewById(R.id.layout_player_container);
_layoutPlayerContainer.onClick.subscribe { onMaximize.emit(false); };
_layoutPlayerContainer = findViewById(R.id.layout_player_container)
_layoutRating = findViewById(R.id.layout_rating);
_textDislikes = findViewById(R.id.text_dislikes);
@@ -612,10 +606,6 @@ class VideoDetailView : ConstraintLayout {
updatePlaybackTracking(position);
};
_player.onVideoClicked.subscribe {
if(_minimizeProgress < 0.5)
onMaximize.emit(false);
}
_player.onSourceChanged.subscribe(::onSourceChanged);
_player.onSourceEnded.subscribe {
if (!fragment.isInPictureInPicture) {
@@ -764,7 +754,7 @@ class VideoDetailView : ConstraintLayout {
MediaControlReceiver.onBackgroundReceived.subscribe(this) {
Logger.i(TAG, "MediaControlReceiver.onBackgroundReceived")
_player.switchToAudioMode(video);
allowBackground = true;
isAudioOnlyUserAction = true;
StateApp.instance.contextOrNull?.let {
try {
if (it is MainActivity) {
@@ -898,6 +888,48 @@ class VideoDetailView : ConstraintLayout {
}
}
}
var currentState = R.id.expanded
rootView.addTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
when (currentId) {
R.id.collapsed -> {
_player.gestureControl.setOnClickListener {
fragment.maximizeVideoDetail(false)
}
_player.gestureControl.controlsEnabled = false
}
R.id.expanded -> {
_layoutResume.alpha = 1f
_cast.setButtonAlpha(1f)
_player.lockControlsAlpha(false)
_player.hideControls(false)
_player.gestureControl.controlsEnabled = true
_player.gestureControl.setOnClickListener(null)
}
}
currentState = currentId
if(currentId == R.id.full_screen_gesture) {
setFullscreen(true)
motionLayout?.transitionToState(R.id.expanded)
}
}
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
if (currentState == R.id.expanded) {
_layoutResume.alpha = 0f
_cast.setButtonAlpha(0f)
_player.lockControlsAlpha(true)
_player.hideControls(true);
}
}
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) { }
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
})
}
val _trackingUpdateTimeLock = Object();
@@ -1009,14 +1041,26 @@ class VideoDetailView : ConstraintLayout {
}
_slideUpOverlay?.hide();
} else null,
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (allowBackground) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
if (!allowBackground) {
if(video is JSVideoDetails && (video as JSVideoDetails).hasVODEvents())
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.vod_chat), TAG_VODCHAT) {
video?.let {
try {
loadVODChat(it);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to reopen vod chat", ex);
}
}
_slideUpOverlay?.hide();
} else null,
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (isAudioOnlyUserAction) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
if (!isAudioOnlyUserAction) {
_player.switchToAudioMode(video);
allowBackground = true;
isAudioOnlyUserAction = true;
it.text.text = resources.getString(R.string.background_revert);
} else {
_player.switchToVideoMode();
allowBackground = false;
isAudioOnlyUserAction = false;
it.text.text = resources.getString(R.string.background);
}
_slideUpOverlay?.hide();
@@ -1132,10 +1176,14 @@ class VideoDetailView : ConstraintLayout {
//Lifecycle
var isLoginStop = false; //TODO: This is a bit jank, but easiest solution for now without reworking flow. (Alternatively, fix MainActivity getting stopped/disposing video)
fun onResume() {
Logger.v(TAG, "onResume");
_onPauseCalled = false;
val wasLoginCall = isLoginStop;
isLoginStop = false;
Logger.i(TAG, "_video: ${video?.name ?: "no video"}");
Logger.i(TAG, "_didStop: $_didStop");
@@ -1144,7 +1192,7 @@ class VideoDetailView : ConstraintLayout {
val t = (lastPositionMilliseconds / 1000.0f).roundToLong();
if(_searchVideo != null)
setVideoOverview(_searchVideo!!, true, t);
else if(_url != null)
else if(_url != null && !wasLoginCall)
setVideo(_url!!, t, _playWhenReady);
}
else if(_didStop) {
@@ -1156,11 +1204,12 @@ class VideoDetailView : ConstraintLayout {
if(_player.isAudioMode) {
//Requested behavior to leave it in audio mode. leaving it commented if it causes issues, revert?
if(!allowBackground) {
if(!isAudioOnlyUserAction) {
_player.switchToVideoMode();
allowBackground = false;
isAudioOnlyUserAction = false;
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.background);
} else {
}
else {
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.video);
}
}
@@ -1178,7 +1227,7 @@ class VideoDetailView : ConstraintLayout {
if(StateCasting.instance.isCasting)
return;
if(allowBackground)
if(isAudioOnlyUserAction)
StatePlayer.instance.startOrUpdateMediaSession(context, video);
else {
when (Settings.instance.playback.backgroundPlay) {
@@ -1186,7 +1235,6 @@ class VideoDetailView : ConstraintLayout {
1 -> {
if(!(video?.isLive ?: false)) {
_player.switchToAudioMode(video);
allowBackground = true;
}
StatePlayer.instance.startOrUpdateMediaSession(context, video);
}
@@ -1974,10 +2022,10 @@ class VideoDetailView : ConstraintLayout {
if (isLimitedVersion && _player.isAudioMode) {
_player.switchToVideoMode()
allowBackground = false;
isAudioOnlyUserAction = false;
} else {
val thumbnail = video.thumbnails.getHQThumbnail();
if ((videoSource == null || _player.isAudioMode) && !thumbnail.isNullOrBlank())
if ((videoSource == null) && !thumbnail.isNullOrBlank()) // || _player.isAudioMode
Glide.with(context).asBitmap().load(thumbnail)
.into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
@@ -2189,16 +2237,6 @@ class VideoDetailView : ConstraintLayout {
}
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
if (ev?.actionMasked == MotionEvent.ACTION_CANCEL ||
ev?.actionMasked == MotionEvent.ACTION_POINTER_DOWN ||
ev?.actionMasked == MotionEvent.ACTION_POINTER_UP) {
onTouchCancel.emit();
}
return super.onInterceptTouchEvent(ev);
}
//Actions
private fun showVideoSettings() {
Logger.i(TAG, "showVideoSettings")
@@ -2692,10 +2730,7 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "handleFullScreen(fullscreen=$fullscreen)")
if(fullscreen) {
_container_content.visibility = GONE
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
val lp = _container_content.layoutParams as LayoutParams;
val lp = _container_content.layoutParams as ConstraintLayout.LayoutParams;
lp.topMargin = 0;
_container_content.layoutParams = lp;
@@ -2706,10 +2741,7 @@ class VideoDetailView : ConstraintLayout {
setProgressBarOverlayed(null);
}
else {
_container_content.visibility = VISIBLE
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
val lp = _container_content.layoutParams as LayoutParams;
val lp = _container_content.layoutParams as ConstraintLayout.LayoutParams;
lp.topMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -18.0f, Resources.getSystem().displayMetrics).toInt();
_container_content.layoutParams = lp;
@@ -2938,7 +2970,7 @@ class VideoDetailView : ConstraintLayout {
hideAddTo()
onVideoClicked.subscribe { video, _ ->
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail()
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail(false)
}
onChannelClicked.subscribe {
@@ -2985,17 +3017,12 @@ class VideoDetailView : ConstraintLayout {
_container_content.visibility = GONE
_player.fillHeight(false)
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
fun handleLeavePictureInPicture() {
Logger.i(TAG, "handleLeavePictureInPicture")
if(!_player.isFullScreen) {
_container_content.visibility = VISIBLE
_player.fitHeight();
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
} else {
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
}
fun getPictureInPictureParams() : PictureInPictureParams {
@@ -3100,53 +3127,6 @@ class VideoDetailView : ConstraintLayout {
}
}
//Animation related setters
fun setMinimizeProgress(progress : Float) {
_minimizeProgress = progress;
_player.lockControlsAlpha(progress < 0.9);
_layoutPlayerContainer.shouldInterceptTouches = progress < 0.95;
if(progress > 0.9) {
if(_minimize_controls.visibility != View.GONE)
_minimize_controls.visibility = View.GONE;
}
else if(_minimize_controls.visibility != View.VISIBLE) {
_minimize_controls.visibility = View.VISIBLE;
}
//Switching video to fill
if(progress > 0.25) {
if(!_player.isFullScreen && _player.layoutParams.height != WRAP_CONTENT) {
_player.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
if(!fragment.isInPictureInPicture) {
_player.fitHeight();
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
}
else {
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
_cast.layoutParams = _cast.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, resources.displayMetrics).toInt();
};
setProgressBarOverlayed(false);
_player.hideControls(false);
}
}
else {
if(_player.layoutParams.height == WRAP_CONTENT) {
_player.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
_player.fillHeight(true)
_cast.layoutParams = _cast.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = 0;
};
setProgressBarOverlayed(true);
_player.hideControls(false);
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
}
}
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
_polycentricProfile = profile
@@ -3170,42 +3150,8 @@ class VideoDetailView : ConstraintLayout {
}
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
Logger.v(TAG, "setProgressBarOverlayed(isOverlayed: ${isOverlayed ?: "null"})");
isOverlayed?.let{ _cast.setProgressBarOverlayed(it) };
if(isOverlayed == null) {
//For now this seems to be the best way to keep it updated?
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -12f, resources.displayMetrics).toInt();
};
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics);
}
else if(isOverlayed) {
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -2f, resources.displayMetrics).toInt();
};
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
}
else {
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6f, resources.displayMetrics).toInt();
};
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics);
}
}
fun setContentAlpha(alpha: Float) {
_container_content.alpha = alpha;
}
fun setControllerAlpha(alpha: Float) {
_layoutResume.alpha = alpha;
_player.videoControls.alpha = alpha;
_cast.setButtonAlpha(alpha);
}
fun setMinimizeControlsAlpha(alpha : Float) {
_minimize_controls.alpha = alpha;
val clickable = alpha > 0.9;
if(_minimize_controls.isClickable != clickable)
_minimize_controls.isClickable = clickable;
Logger.v(TAG, "setProgressBarOverlayed(isOverlayed: ${isOverlayed ?: "null"})")
isOverlayed?.let { _cast.setProgressBarOverlayed(it) }
}
override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -3217,16 +3163,6 @@ class VideoDetailView : ConstraintLayout {
}
}
fun setVideoMinimize(value : Float) {
val padRight = (resources.displayMetrics.widthPixels * 0.70 * value).toInt()
_player.setPadding(0, _player.paddingTop, padRight, 0)
_cast.setPadding(0, _cast.paddingTop, padRight, 0)
}
fun setTopPadding(value: Float) {
_player.setPadding(_player.paddingLeft, value.toInt(), _player.paddingRight, 0)
}
//Tasks
private val _taskLoadVideo = if(!isInEditMode) TaskHandler<String, IPlatformVideoDetails>(
StateApp.instance.scopeGetter,
@@ -3267,8 +3203,13 @@ class VideoDetailView : ConstraintLayout {
val id = e.config.let { if(it is SourcePluginConfig) it.id else null };
val didLogin = if(id == null)
false
else StatePlugins.instance.loginPlugin(context, id) {
fetchVideo();
else {
isLoginStop = true;
StatePlugins.instance.loginPlugin(context, id) {
fragment.lifecycleScope.launch(Dispatchers.Main) {
fetchVideo();
}
}
}
if(!didLogin)
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login");
@@ -3446,6 +3387,7 @@ class VideoDetailView : ConstraintLayout {
const val TAG_SHARE = "share";
const val TAG_OVERLAY = "overlay";
const val TAG_LIVECHAT = "livechat";
const val TAG_VODCHAT = "vodchat";
const val TAG_CHAPTERS = "chapters";
const val TAG_OPEN = "open";
const val TAG_SEND_TO_DEVICE = "send_to_device";
@@ -0,0 +1,454 @@
package com.futo.platformplayer.fragment.mainactivity.special
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.text.Spanned
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Button
import android.widget.FrameLayout
import android.widget.FrameLayout.GONE
import android.widget.FrameLayout.VISIBLE
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.net.toUri
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.MonetizationView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.DescriptionOverlay
import com.futo.platformplayer.views.overlays.RepliesOverlay
import com.futo.platformplayer.views.overlays.SupportOverlay
import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.platformplayer.views.segments.CommentsList
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Models
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class CommentsModalBottomSheet : BottomSheetDialogFragment() {
var mainFragment: MainFragment? = null
private lateinit var containerContent: FrameLayout
private lateinit var containerContentMain: LinearLayout
private lateinit var containerContentReplies: RepliesOverlay
private lateinit var containerContentDescription: DescriptionOverlay
private lateinit var containerContentSupport: SupportOverlay
private lateinit var title: TextView
private lateinit var subTitle: TextView
private lateinit var channelName: TextView
private lateinit var channelMeta: TextView
private lateinit var creatorThumbnail: CreatorThumbnail
private lateinit var channelButton: LinearLayout
private lateinit var monetization: MonetizationView
private lateinit var platform: PlatformIndicator
private lateinit var textLikes: TextView
private lateinit var textDislikes: TextView
private lateinit var layoutRating: LinearLayout
private lateinit var imageDislikeIcon: ImageView
private lateinit var imageLikeIcon: ImageView
private lateinit var description: TextView
private lateinit var descriptionContainer: LinearLayout
private lateinit var descriptionViewMore: TextView
private lateinit var commentsList: CommentsList
private lateinit var addCommentView: AddCommentView
private var polycentricProfile: PolycentricProfile? = null
private lateinit var buttonPolycentric: Button
private lateinit var buttonPlatform: Button
private var tabIndex: Int? = null
private var contentOverlayView: View? = null
lateinit var video: IPlatformVideoDetails
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
private val _taskLoadPolycentricProfile =
TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(
ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }).success { setPolycentricProfile(it, animate = true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it)
}
override fun onCreateDialog(
savedInstanceState: Bundle?,
): Dialog {
val bottomSheetDialog =
BottomSheetDialog(requireContext(), R.style.Custom_BottomSheetDialog_Theme)
bottomSheetDialog.setContentView(R.layout.modal_comments)
behavior = bottomSheetDialog.behavior
// TODO figure out how to not need all of these non null assertions
containerContent = bottomSheetDialog.findViewById(R.id.content_container)!!
containerContentMain = bottomSheetDialog.findViewById(R.id.videodetail_container_main)!!
containerContentReplies =
bottomSheetDialog.findViewById(R.id.videodetail_container_replies)!!
containerContentDescription =
bottomSheetDialog.findViewById(R.id.videodetail_container_description)!!
containerContentSupport =
bottomSheetDialog.findViewById(R.id.videodetail_container_support)!!
title = bottomSheetDialog.findViewById(R.id.videodetail_title)!!
subTitle = bottomSheetDialog.findViewById(R.id.videodetail_meta)!!
channelName = bottomSheetDialog.findViewById(R.id.videodetail_channel_name)!!
channelMeta = bottomSheetDialog.findViewById(R.id.videodetail_channel_meta)!!
creatorThumbnail = bottomSheetDialog.findViewById(R.id.creator_thumbnail)!!
channelButton = bottomSheetDialog.findViewById(R.id.videodetail_channel_button)!!
monetization = bottomSheetDialog.findViewById(R.id.monetization)!!
platform = bottomSheetDialog.findViewById(R.id.videodetail_platform)!!
layoutRating = bottomSheetDialog.findViewById(R.id.layout_rating)!!
textDislikes = bottomSheetDialog.findViewById(R.id.text_dislikes)!!
textLikes = bottomSheetDialog.findViewById(R.id.text_likes)!!
imageLikeIcon = bottomSheetDialog.findViewById(R.id.image_like_icon)!!
imageDislikeIcon = bottomSheetDialog.findViewById(R.id.image_dislike_icon)!!
description = bottomSheetDialog.findViewById(R.id.videodetail_description)!!
descriptionContainer =
bottomSheetDialog.findViewById(R.id.videodetail_description_container)!!
descriptionViewMore =
bottomSheetDialog.findViewById(R.id.videodetail_description_view_more)!!
addCommentView = bottomSheetDialog.findViewById(R.id.add_comment_view)!!
commentsList = bottomSheetDialog.findViewById(R.id.comments_list)!!
buttonPolycentric = bottomSheetDialog.findViewById(R.id.button_polycentric)!!
buttonPlatform = bottomSheetDialog.findViewById(R.id.button_platform)!!
commentsList.onAuthorClick.subscribe { c ->
if (c !is PolycentricPlatformComment) {
return@subscribe
}
val id = c.author.id.value
Logger.i(TAG, "onAuthorClick: $id")
if (id != null && id.startsWith("polycentric://")) {
val navUrl = "https://harbor.social/" + id.substring("polycentric://".length)
mainFragment!!.startActivity(Intent(Intent.ACTION_VIEW, navUrl.toUri()))
}
}
commentsList.onRepliesClick.subscribe { c ->
val replyCount = c.replyCount ?: 0
var metadata = ""
if (replyCount > 0) {
metadata += "$replyCount " + requireContext().getString(R.string.replies)
}
if (c is PolycentricPlatformComment) {
var parentComment: PolycentricPlatformComment = c
containerContentReplies.load(tabIndex!! != 0, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, {
val newComment = parentComment.cloneWithUpdatedReplyCount(
(parentComment.replyCount ?: 0) + 1
)
commentsList.replaceComment(parentComment, newComment)
parentComment = newComment
})
} else {
containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) })
}
animateOpenOverlayView(containerContentReplies)
}
if (StatePolycentric.instance.enabled) {
buttonPolycentric.setOnClickListener {
setTabIndex(0)
StateMeta.instance.setLastCommentSection(0)
}
} else {
buttonPolycentric.visibility = GONE
}
buttonPlatform.setOnClickListener {
setTabIndex(1)
StateMeta.instance.setLastCommentSection(1)
}
val ref = Models.referenceFromBuffer(video.url.toByteArray())
addCommentView.setContext(video.url, ref)
if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) {
setTabIndex(2, true)
} else {
when (Settings.instance.comments.defaultCommentSection) {
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
1 -> setTabIndex(1, true)
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
}
}
containerContentDescription.onClose.subscribe { animateCloseOverlayView() }
containerContentReplies.onClose.subscribe { animateCloseOverlayView() }
descriptionViewMore.setOnClickListener {
animateOpenOverlayView(containerContentDescription)
}
updateDescriptionUI(video.description.fixHtmlLinks())
val dp5 =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics)
val dp2 =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics)
//UI
title.text = video.name
channelName.text = video.author.name
if (video.author.subscribers != null) {
channelMeta.text = if ((video.author.subscribers
?: 0) > 0
) video.author.subscribers!!.toHumanNumber() + " " + requireContext().getString(R.string.subscribers) else ""
(channelName.layoutParams as MarginLayoutParams).setMargins(
0, (dp5 * -1).toInt(), 0, 0
)
} else {
channelMeta.text = ""
(channelName.layoutParams as MarginLayoutParams).setMargins(0, (dp2).toInt(), 0, 0)
}
video.author.let {
if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl)
else monetization.setPlatformMembership(null, null)
}
val subTitleSegments: ArrayList<String> = ArrayList()
if (video.viewCount > 0) subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if (video.isLive) requireContext().getString(
R.string.watching_now) else requireContext().getString(R.string.views)}")
if (video.datetime != null) {
val diff = video.datetime?.getNowDiffSeconds() ?: 0
val ago = video.datetime?.toHumanNowDiffString(true)
if (diff >= 0) subTitleSegments.add("$ago ago")
else subTitleSegments.add("available in $ago")
}
platform.setPlatformFromClientID(video.id.pluginId)
subTitle.text = subTitleSegments.joinToString("")
creatorThumbnail.setThumbnail(video.author.thumbnail, false)
setPolycentricProfile(null, animate = false)
_taskLoadPolycentricProfile.run(video.author.id)
when (video.rating) {
is RatingLikeDislikes -> {
val r = video.rating as RatingLikeDislikes
layoutRating.visibility = VISIBLE
textLikes.visibility = VISIBLE
imageLikeIcon.visibility = VISIBLE
textLikes.text = r.likes.toHumanNumber()
imageDislikeIcon.visibility = VISIBLE
textDislikes.visibility = VISIBLE
textDislikes.text = r.dislikes.toHumanNumber()
}
is RatingLikes -> {
val r = video.rating as RatingLikes
layoutRating.visibility = VISIBLE
textLikes.visibility = VISIBLE
imageLikeIcon.visibility = VISIBLE
textLikes.text = r.likes.toHumanNumber()
imageDislikeIcon.visibility = GONE
textDislikes.visibility = GONE
}
else -> {
layoutRating.visibility = GONE
}
}
monetization.onSupportTap.subscribe {
containerContentSupport.setPolycentricProfile(polycentricProfile)
animateOpenOverlayView(containerContentSupport)
}
monetization.onStoreTap.subscribe {
polycentricProfile?.systemState?.store?.let {
try {
val uri = it.toUri()
val intent = Intent(Intent.ACTION_VIEW)
intent.data = uri
requireContext().startActivity(intent)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to open URI: '${it}'.", e)
}
}
}
monetization.onUrlTap.subscribe {
mainFragment!!.navigate<BrowserFragment>(it)
}
addCommentView.onCommentAdded.subscribe {
commentsList.addComment(it)
}
channelButton.setOnClickListener {
mainFragment!!.navigate<ChannelFragment>(video.author)
}
return bottomSheetDialog
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
animateCloseOverlayView()
}
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
polycentricProfile = profile
val dp35 = 35.dp(requireContext().resources)
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
if (avatar != null) {
creatorThumbnail.setThumbnail(avatar, animate)
} else {
creatorThumbnail.setThumbnail(video.author.thumbnail, animate)
creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto())
}
val username = profile?.systemState?.username
if (username != null) {
channelName.text = username
}
monetization.setPolycentricProfile(profile)
}
private fun setTabIndex(index: Int?, forceReload: Boolean = false) {
Logger.i(TAG, "setTabIndex (index: ${index}, forceReload: ${forceReload})")
val changed = tabIndex != index || forceReload
if (!changed) {
return
}
tabIndex = index
buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac, null))
buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac, null))
when (index) {
null -> {
addCommentView.visibility = GONE
commentsList.clear()
}
0 -> {
addCommentView.visibility = VISIBLE
fetchPolycentricComments()
}
1 -> {
addCommentView.visibility = GONE
fetchComments()
}
}
}
private fun fetchComments() {
Logger.i(TAG, "fetchComments")
video.let {
commentsList.load(true) { StatePlatform.instance.getComments(it) }
}
}
private fun fetchPolycentricComments() {
Logger.i(TAG, "fetchPolycentricComments")
val video = video
val idValue = video.id.value
if (video.url.isEmpty()) {
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
commentsList.clear()
return
}
val ref = Models.referenceFromBuffer(video.url.toByteArray())
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); }
}
private fun updateDescriptionUI(text: Spanned) {
containerContentDescription.load(text)
description.text = text
if (description.text.isNotEmpty()) descriptionContainer.visibility = VISIBLE
else descriptionContainer.visibility = GONE
}
private fun animateOpenOverlayView(view: View) {
if (contentOverlayView != null) {
Logger.e(TAG, "Content overlay already open")
return
}
behavior.isDraggable = false
behavior.state = BottomSheetBehavior.STATE_EXPANDED
val animHeight = containerContentMain.height
view.translationY = animHeight.toFloat()
view.visibility = VISIBLE
view.animate().setDuration(300).translationY(0f).withEndAction {
contentOverlayView = view
}.start()
}
private fun animateCloseOverlayView() {
val curView = contentOverlayView
if (curView == null) {
Logger.e(TAG, "No content overlay open")
return
}
behavior.isDraggable = true
val animHeight = contentOverlayView!!.height
curView.animate().setDuration(300).translationY(animHeight.toFloat()).withEndAction {
curView.visibility = GONE
contentOverlayView = null
}.start()
}
companion object {
const val TAG = "ModalBottomSheet"
}
}
@@ -194,17 +194,18 @@ class StateHistory {
_remoteHistoryDatesStore.save();
}
fun syncRemoteHistory(plugin: JSClient) {
fun syncRemoteHistory(plugin: JSClient): Int {
if (plugin.capabilities.hasGetUserHistory &&
plugin.isLoggedIn) {
Logger.i(TAG, "Syncing remote history for plugin [${plugin.name}]");
val hist = StatePlatform.instance.getUserHistory(plugin.id);
syncRemoteHistory(plugin.id, hist, 100, 3);
return syncRemoteHistory(plugin.id, hist, 100, 3);
}
return 0;
}
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int) {
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int): Int {
val lastDate = _remoteHistoryDatesStore.get(pluginId) ?: OffsetDateTime.MIN;
val maxVideosCount = if(maxVideos <= 0) 500 else maxVideos;
val maxPageCount = if(maxPages <= 0) 3 else maxPages;
@@ -272,12 +273,14 @@ class StateHistory {
}
catch(ex: Throwable){}
}
return updated;
}
}
catch(ex: Throwable) {
val plugin = if(pluginId != StateDeveloper.DEV_ID) StatePlugins.instance.getPlugin(pluginId) else null;
Logger.e(TAG, "Sync Remote History failed for [${plugin?.config?.name}] due to: " + ex.message)
}
return 0;
}
companion object {
@@ -500,7 +500,7 @@ class StatePlatform {
.toList()
.associateWith { 1f };
val pager = MultiDistributionContentPager(pages);
val pager = MultiDistributionContentPager(pages, 2);
pager.initialize();
return pager;
}
@@ -179,8 +179,9 @@ class StatePlugins {
}
StateApp.instance.scope.launch(Dispatchers.IO) {
StatePlatform.instance.reloadClient(context, id);
afterLogin.invoke();
StatePlatform.instance.reloadClient(context, id) {
afterLogin.invoke();
}
}
};
return true;
@@ -475,6 +476,7 @@ class StatePlugins {
delay(500);
val client = ManagedHttpClient();
client.setTimeout(10000);
try {
withContext(Dispatchers.Main) {
onProgress.invoke("Validating script", 0.25);
@@ -489,14 +491,14 @@ class StatePlugins {
}
val icon = config.absoluteIconUrl?.let { absIconUrl ->
withContext(Dispatchers.Main) {
onProgress.invoke("Saving plugin", 0.75);
}
val iconResp = client.get(absIconUrl);
if (iconResp.isOk)
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
return@let null;
}
withContext(Dispatchers.Main) {
onProgress.invoke("Saving plugin", 0.75);
}
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
if (installEx != null)
throw installEx;
@@ -119,6 +119,8 @@ class GestureControlView : LinearLayout {
var fullScreenGestureEnabled = true
var controlsEnabled = true
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_gesture_controls, this, true);
@@ -350,6 +352,10 @@ class GestureControlView : LinearLayout {
override fun onTouchEvent(event: MotionEvent?): Boolean {
val ev = event ?: return super.onTouchEvent(event);
if(!controlsEnabled) {
return super.onTouchEvent(ev)
}
if (ev.action == MotionEvent.ACTION_UP && _speedHolding) {
_speedHolding = false
hideHoldSpeedControls()
@@ -0,0 +1,117 @@
package com.futo.platformplayer.views.buttons
import android.content.Context
import android.graphics.Bitmap
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.constructs.Event0
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.ShapeAppearanceModel
class ShortsButton : LinearLayout {
private val _root: LinearLayout;
private val _icon: ImageView;
private val _textPrimary: TextView;
val onClick = Event0();
var iconId: Int? = null;
constructor(context : Context, text: String, icon: Int, action: ()->Unit) : super(context) {
inflate(context, R.layout.view_shorts_button, this);
_icon = findViewById(R.id.button_icon);
_textPrimary = findViewById(R.id.button_text);
_root = findViewById(R.id.root);
withPrimaryText(text);
withIcon(icon);
_root.apply {
isClickable = true;
setOnClickListener {
if(!isEnabled)
return@setOnClickListener;
action();
onClick.emit();
UIDialogs.toast("Clicked button: " + _textPrimary.text);
};
}
}
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_shorts_button, this);
_icon = findViewById(R.id.image_icon);
_textPrimary = findViewById(R.id.text_title);
_root = findViewById(R.id.root);
_root.apply {
isClickable = true;
setOnClickListener {
if(!isEnabled)
return@setOnClickListener;
onClick.emit();
};
}
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.ShortsButton, 0, 0);
val attrIconRef = attrArr.getResourceId(R.styleable.ShortsButton_buttonIcon_s, -1);
val attrText = attrArr.getText(R.styleable.ShortsButton_buttonText_s) ?: "";
attrArr.recycle()
withIcon(attrIconRef);
withPrimaryText(attrText.toString());
}
fun withMargin(bottom: Int, side: Int = 0): ShortsButton {
setPadding(side, 0, side, bottom)
return this;
}
fun withPrimaryText(text: String): ShortsButton {
_textPrimary.text = text;
if(text.isNullOrBlank())
_textPrimary.visibility = View.GONE;
else
_textPrimary.visibility = View.VISIBLE;
return this;
}
fun withIcon(resourceId: Int): ShortsButton {
if (resourceId != -1) {
_icon.visibility = View.VISIBLE;
_icon.setImageResource(resourceId);
} else
_icon.visibility = View.GONE;
_icon.scaleType = ImageView.ScaleType.CENTER_CROP;
iconId = resourceId;
return this;
}
fun withIcon(bitmap: Bitmap): ShortsButton {
_icon.visibility = View.VISIBLE;
_icon.setImageBitmap(bitmap);
iconId = -1;
_icon.scaleType = ImageView.ScaleType.CENTER_CROP;
return this;
}
fun setButtonEnabled(enabled: Boolean) {
if(enabled) {
alpha = 1f;
isEnabled = true;
isClickable = true;
}
else {
alpha = 0.5f;
isEnabled = false;
isClickable = false;
}
}
}
@@ -0,0 +1,121 @@
package com.futo.platformplayer.views.containers
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.constraintlayout.motion.widget.MotionLayout
import com.futo.platformplayer.R
import kotlin.math.abs
class CustomMotionLayout(context: Context, attributeSet: AttributeSet? = null) :
MotionLayout(context, attributeSet) {
private val viewToDetectTouch by lazy {
findViewById<View>(R.id.layout_player_container) //TODO move to Attributes
}
private val viewToDetectTouch2 by lazy {
findViewById<View>(R.id.minimize_controls) //TODO move to Attributes
}
private var savedActionDown: MotionEvent? = null
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
// intercepting touch events is necessary because something to do with PlayerControlView makes things not work
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
val ev = event ?: return super.onInterceptTouchEvent(null)
// special touch interception logic is unnecessary if interaction is disabled
if (!isInteractionEnabled) {
return super.onInterceptTouchEvent(ev)
}
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val viewRect = Rect()
viewToDetectTouch.getHitRect(viewRect)
val isInView = viewRect.contains(ev.x.toInt(), ev.y.toInt())
viewToDetectTouch2.getHitRect(viewRect)
val isInView2 = viewRect.contains(ev.x.toInt(), ev.y.toInt())
// Don't intercept touches if they are outside of the player or the mini player controls
if (!isInView && !isInView2) {
return false
}
val thing = super.onInterceptTouchEvent(ev)
// If the MotionLayout is already intercepting this touch then don't track it
if (thing) {
return true
}
// MotionLayout didn't intercept the touch but the touch is over the player/mini controls views
// in the future the class will
// save the touch event for later
// need to replay this initial touch to the MotionLayout if it ends up turning into a drag
// return false because that matches the return from the super call above
savedActionDown?.recycle() // Recycle the old event to prevent memory leaks (if for some reason it wasn't cleaned up in the other code paths)
savedActionDown = MotionEvent.obtain(ev)
return false
}
MotionEvent.ACTION_MOVE -> {
val localSavedActionDown = savedActionDown
// only handle the move event if there is a saved action stored
// then check to see if it has turned into a drag
if (localSavedActionDown != null) {
val dy = abs(ev.y - localSavedActionDown.y)
if (dy > touchSlop) {
// if it has turned into a drag then
// replay the down action saved earlier
// clean up our data
// return true so that the MotionLayout's onTouchEvent will receive future events for this gesture
//
// it is necessary to replay the down action because otherwise MotionLayout will not always initialize the drag correctly
super.onTouchEvent(localSavedActionDown)
localSavedActionDown.recycle() // Clean up the saved event after replaying
savedActionDown = null
return true
}
}
}
// if it's an up or cancel action clean up our tracking
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
savedActionDown?.recycle()
savedActionDown = null
}
}
// since the function hasn't handled the even this far send it to the parent class
return super.onInterceptTouchEvent(ev)
}
// onTouchEvent is necessary to make sure that only touch and drag on the video triggers the animation (instead of everywhere on the screen)
@SuppressLint("ClickableViewAccessibility") // pretty sure this issue doesn't apply
override fun onTouchEvent(event: MotionEvent?): Boolean {
val ev = event ?: return super.onTouchEvent(null)
// special touch event handling logic is unnecessary if interaction is disabled
if (!isInteractionEnabled) {
return super.onTouchEvent(ev)
}
val viewRect = Rect()
viewToDetectTouch.getHitRect(viewRect)
val isInView = viewRect.contains(ev.x.toInt(), ev.y.toInt())
viewToDetectTouch2.getHitRect(viewRect)
val isInView2 = viewRect.contains(ev.x.toInt(), ev.y.toInt())
// don't want to handle touches outside of the player/mini controls views
if ((!isInView && !isInView2) && event.actionMasked == MotionEvent.ACTION_DOWN) {
return false
}
return super.onTouchEvent(event)
}
}
@@ -1,94 +0,0 @@
package com.futo.platformplayer.views.containers
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.motion.widget.MotionLayout
import com.futo.platformplayer.R
class SingleViewTouchableMotionLayout(context: Context, attributeSet: AttributeSet? = null) : MotionLayout(context, attributeSet) {
private val viewToDetectTouch by lazy {
findViewById<View>(R.id.touchContainer) //TODO move to Attributes
}
private val viewRect = Rect()
private var touchStarted = false
private val transitionListenerList = mutableListOf<TransitionListener?>()
var allowMotion : Boolean = true;
init {
addTransitionListener(object : TransitionListener {
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
}
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
touchStarted = false
}
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
}
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {
}
})
super.setTransitionListener(object : TransitionListener {
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
transitionListenerList.filterNotNull()
.forEach { it.onTransitionChange(p0, p1, p2, p3) }
}
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
transitionListenerList.filterNotNull()
.forEach { it.onTransitionCompleted(p0, p1) }
}
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
}
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {
}
})
//isInteractionEnabled = false;
}
override fun setTransitionListener(listener: TransitionListener?) {
addTransitionListener(listener)
}
override fun addTransitionListener(listener: TransitionListener?) {
transitionListenerList += listener
}
//This always triggers, workaround calling super.onTouchEvent
//Blocks click events underneath
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
if(!allowMotion)
return false;
if(event != null) {
when (event.actionMasked) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
touchStarted = false
return super.onTouchEvent(event) && false;
}
}
if (!touchStarted) {
viewToDetectTouch.getHitRect(viewRect);
val isInView = viewRect.contains(event.x.toInt(), event.y.toInt());
touchStarted = isInView
}
}
return touchStarted && super.onTouchEvent(event) && false;
}
//Not triggered on its own due to child views, intercept is used instead.
override fun onTouchEvent(event: MotionEvent): Boolean {
return false;
}
}
@@ -14,6 +14,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.getDataLinkFromUrl
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.IdenticonView
import userpackage.Protocol
@@ -82,14 +83,14 @@ class CreatorThumbnail : ConstraintLayout {
Glide.with(_imageChannelThumbnail)
.load(url)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.crossfade()
.into(_imageChannelThumbnail);
.into(_imageChannelThumbnail)
} else {
Glide.with(_imageChannelThumbnail)
.load(url)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(_imageChannelThumbnail);
}
}
@@ -6,6 +6,7 @@ import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.animation.LinearInterpolator
import androidx.annotation.Dimension
import androidx.annotation.OptIn
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
@@ -65,6 +66,8 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
videoView = findViewById(R.id.short_player_view)
progressBar = findViewById(R.id.short_player_progress_bar)
videoView.subtitleView?.setFixedTextSize(Dimension.SP, 18F);
if (!isInEditMode) {
player = StatePlayer.instance.getShortPlayerOrCreate(context)
player.player.repeatMode = Player.REPEAT_MODE_ONE
@@ -125,7 +125,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private var _lastSourceFit: Float? = null;
private var _lastWindowWidth: Int = resources.configuration.screenWidthDp
private var _lastWindowHeight: Int = resources.configuration.screenHeightDp
private var _originalBottomMargin: Int = 0;
private var _isControlsLocked: Boolean = false;
@@ -154,10 +153,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onSourceEnded = Event0();
val onPrevious = Event0();
val onNext = Event0();
val onChapterChanged = Event2<IChapter?, Boolean>();
val onVideoClicked = Event0();
val onTimeBarChanged = Event2<Long, Long>();
val onChapterClicked = Event1<IChapter>();
@@ -650,13 +646,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
if (fullScreen) {
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
lp.bottomMargin = 0;
background.layoutParams = lp;
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.0f, Resources.getSystem().displayMetrics).toInt())
_videoView.setBackgroundColor(Color.parseColor("#FF000000"))
gestureControl.hideControls();
//videoControlsBar.visibility = View.GONE;
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
_videoControls_fullscreen.show();
@@ -664,13 +657,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
videoControls.visibility = View.GONE;
}
else {
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
lp.bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt();
background.layoutParams = lp;
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
_videoView.setBackgroundColor(Color.parseColor("#00000000"))
gestureControl.hideControls();
//videoControlsBar.visibility = View.VISIBLE;
_videoView.resizeMode = _desiredResizeModePortrait;
videoControls.show();
@@ -701,10 +691,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_isControlsLocked = locked;
}
override fun play() {
super.play();
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
gestureControl.resetZoomPan()
_lastSourceFit = null;
@@ -774,11 +760,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
@OptIn(UnstableApi::class)
fun fitHeight(videoSize: VideoSize? = null) {
Logger.i(TAG, "Video Fit Height")
if (_originalBottomMargin != 0) {
val layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams
layoutParams.setMargins(0, 0, 0, _originalBottomMargin)
_videoView.layoutParams = layoutParams
}
var h = videoSize?.height ?: lastVideoSource?.height ?: exoPlayer?.player?.videoSize?.height
?: 0
@@ -819,15 +800,13 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
_videoView.resizeMode = _desiredResizeModePortrait
val marginBottom =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics)
val height = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
_lastSourceFit!!,
resources.displayMetrics
)
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, (height + marginBottom).toInt())
rootParams.bottomMargin = marginBottom.toInt()
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, (height).toInt())
_root.layoutParams = rootParams
isFitMode = true
}
@@ -835,9 +814,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
@OptIn(UnstableApi::class)
fun fillHeight(isMiniPlayer: Boolean) {
Logger.i(TAG, "Video Fill Height");
val layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams;
_originalBottomMargin =
if (layoutParams.bottomMargin > 0) layoutParams.bottomMargin else _originalBottomMargin;
var layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams;
layoutParams.setMargins(0);
_videoView.layoutParams = layoutParams;
_videoView.invalidate();
@@ -848,16 +825,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
if(isMiniPlayer){
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
} else {
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.0f, Resources.getSystem().displayMetrics).toInt())
}
isFitMode = false;
}
//Animated Calls
fun setEndPadding(value: Float) {
setPadding(0, 0, value.toInt(), 0)
}
fun updateRotateLock() {
_control_rotate_lock.visibility = View.VISIBLE;
_control_rotate_lock_fullscreen.visibility = View.VISIBLE;
@@ -907,11 +882,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
override fun switchToVideoMode() {
super.switchToVideoMode()
setArtwork(null)
//setArtwork(null)
}
override fun switchToAudioMode(video: IPlatformVideoDetails?) {
super.switchToAudioMode(video)
//This causes issues, and is in general confusing, needs improvements
/*
val thumbnail = video?.thumbnails?.getHQThumbnail()
if (!thumbnail.isNullOrBlank()) {
Glide.with(context).asBitmap().load(thumbnail)
@@ -928,5 +906,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
})
}
*/
}
}
@@ -873,7 +873,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
@OptIn(UnstableApi::class)
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
val sourceVideo = _lastVideoMediaSource
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
val sourceAudio = _lastAudioMediaSource;
val sourceSubs = _lastSubtitleMediaSource;
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:strokeColor="#222"
android:strokeWidth="20"
android:pathData="M240,560L720,560L720,480L240,480L240,560ZM240,440L720,440L720,360L240,360L240,440ZM240,320L720,320L720,240L240,240L240,320ZM880,880L720,720L160,720Q127,720 103.5,696.5Q80,673 80,640L80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,880ZM160,640L754,640L800,685L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640ZM160,640Q160,640 160,640Q160,640 160,640L160,160Q160,160 160,160Q160,160 160,160L160,160Q160,160 160,160Q160,160 160,160L160,640Z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:strokeColor="#222"
android:strokeWidth="20"
android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:strokeColor="#222"
android:strokeWidth="20"
android:pathData="M680,880Q630,880 595,845Q560,810 560,760Q560,754 563,732L282,568Q266,583 245,591.5Q224,600 200,600Q150,600 115,565Q80,530 80,480Q80,430 115,395Q150,360 200,360Q224,360 245,368.5Q266,377 282,392L563,228Q561,221 560.5,214.5Q560,208 560,200Q560,150 595,115Q630,80 680,80Q730,80 765,115Q800,150 800,200Q800,250 765,285Q730,320 680,320Q656,320 635,311.5Q614,303 598,288L317,452Q319,459 319.5,465.5Q320,472 320,480Q320,488 319.5,494.5Q319,501 317,508L598,672Q614,657 635,648.5Q656,640 680,640Q730,640 765,675Q800,710 800,760Q800,810 765,845Q730,880 680,880ZM680,800Q697,800 708.5,788.5Q720,777 720,760Q720,743 708.5,731.5Q697,720 680,720Q663,720 651.5,731.5Q640,743 640,760Q640,777 651.5,788.5Q663,800 680,800ZM200,520Q217,520 228.5,508.5Q240,497 240,480Q240,463 228.5,451.5Q217,440 200,440Q183,440 171.5,451.5Q160,463 160,480Q160,497 171.5,508.5Q183,520 200,520ZM680,240Q697,240 708.5,228.5Q720,217 720,200Q720,183 708.5,171.5Q697,160 680,160Q663,160 651.5,171.5Q640,183 640,200Q640,217 651.5,228.5Q663,240 680,240ZM680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760ZM200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480ZM680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:strokeColor="#222"
android:strokeWidth="20"
android:pathData="M240,120L680,120L680,640L400,920L350,870Q343,863 338.5,851Q334,839 334,828L334,814L378,640L120,640Q88,640 64,616Q40,592 40,560L40,480Q40,473 42,465Q44,457 46,450L166,168Q175,148 196,134Q217,120 240,120ZM600,200L240,200Q240,200 240,200Q240,200 240,200L120,480L120,560Q120,560 120,560Q120,560 120,560L480,560L426,780L600,606L600,200ZM600,606L600,606L600,560L600,560Q600,560 600,560Q600,560 600,560L600,480L600,200Q600,200 600,200Q600,200 600,200L600,200L600,606ZM680,640L680,560L800,560L800,200L680,200L680,120L880,120L880,640L680,640Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@color/colorPrimary"
android:pathData="M240,120L640,120L640,640L360,920L310,870Q303,863 298.5,851Q294,839 294,828L294,814L338,640L120,640Q88,640 64,616Q40,592 40,560L40,480Q40,473 41.5,465Q43,457 46,450L166,168Q175,148 196,134Q217,120 240,120ZM720,640L720,120L880,120L880,640L720,640Z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:strokeColor="#222"
android:strokeWidth="20"
android:pathData="M720,840L280,840L280,320L560,40L610,90Q617,97 621.5,109Q626,121 626,132L626,146L582,320L840,320Q872,320 896,344Q920,368 920,400L920,480Q920,487 918,495Q916,503 914,510L794,792Q785,812 764,826Q743,840 720,840ZM360,760L720,760Q720,760 720,760Q720,760 720,760L840,480L840,400Q840,400 840,400Q840,400 840,400L480,400L534,180L360,354L360,760ZM360,354L360,354L360,400L360,400Q360,400 360,400Q360,400 360,400L360,480L360,760Q360,760 360,760Q360,760 360,760L360,760L360,354ZM280,320L280,400L160,400L160,760L280,760L280,840L80,840L80,320L280,320Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@color/colorPrimary"
android:pathData="M720,840L320,840L320,320L600,40L650,90Q657,97 661.5,109Q666,121 666,132L666,146L622,320L840,320Q872,320 896,344Q920,368 920,400L920,480Q920,487 918.5,495Q917,503 914,510L794,792Q785,812 764,826Q743,840 720,840ZM240,320L240,840L80,840L80,320L240,320Z"/>
</vector>
@@ -55,7 +55,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragview_video_detail"
android:elevation="15dp"
android:visibility="invisible" />
</FrameLayout>
@@ -94,10 +94,13 @@
android:layout_width="match_parent"
android:layout_height="0dp" />
<!-- TODO: the padding for the recycler view needs to be the same as the minimized video player and perhaps should be dynamic based on whether the player is mini or closed-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="80dp"
android:clipToPadding="false"
android:orientation="vertical" />
</LinearLayout>
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/transparent"
app:layoutDescription="@xml/videodetail_scene"
app:layout_collapseMode="parallax">
<androidx.cardview.widget.CardView
android:id="@+id/touchContainer"
android:layout_width="match_parent"
android:layout_height="220dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="#222222" />
<com.futo.platformplayer.fragment.mainactivity.main.VideoDetailView
android:id="@+id/fragview_videodetail"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/touchContainer"
app:layout_constraintEnd_toEndOf="@+id/touchContainer"
app:layout_constraintStart_toStartOf="@+id/touchContainer"
app:layout_constraintTop_toTopOf="@+id/touchContainer"
android:nestedScrollingEnabled="false" />
</com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout>
@@ -1,24 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.futo.platformplayer.views.containers.CustomMotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:background="@color/transparent"
android:id="@+id/videodetail_root"
android:clickable="true">
app:layoutDescription="@xml/videodetail_scene">
<com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
<FrameLayout
android:id="@+id/layout_player_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="6dp"
android:elevation="2dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
android:layout_width="0dp"
android:layout_height="0dp">
<!--this acts as a background-->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardBackgroundColor="@color/black"
android:translationY="-7dp"/>
<com.futo.platformplayer.views.video.FutoVideoPlayer
android:id="@+id/videodetail_player"
@@ -38,39 +40,31 @@
android:visibility="gone"
android:elevation="4dp"
android:layout_marginBottom="6dp" />
</FrameLayout>
<androidx.media3.ui.PlayerControlView
android:id="@+id/videodetail_progress"
android:layout_width="match_parent"
android:layout_height="12dp"
android:layout_gravity="bottom"
android:layout_marginLeft="-6dp"
android:layout_marginRight="-6dp"
android:layout_marginBottom="6dp"
app:show_timeout="-1"
android:elevation="2dp"
app:controller_layout_id="@layout/video_player_ui_bar" />
</com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout>
<androidx.media3.ui.PlayerControlView
android:id="@+id/videodetail_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:show_timeout="-1"
android:elevation="1dp"
android:background="@color/black"
app:controller_layout_id="@layout/video_player_ui_bar" />
<!--Minimized Controls-->
<LinearLayout
android:id="@+id/minimize_controls"
android:orientation="horizontal"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_height="0dp"
android:paddingBottom="7dp"
android:gravity="center_vertical"
android:paddingStart="10dp"
android:clickable="false"
android:elevation="5dp"
android:alpha="1"
app:layout_constraintTop_toTopOf="@id/layout_player_container"
app:layout_constraintBottom_toBottomOf="@id/layout_player_container"
app:layout_constraintEnd_toEndOf="@id/layout_player_container"
app:layout_constraintWidth_percent="0.7">
android:background="@color/black"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:orientation="vertical">
<!--Video Title-->
@@ -180,16 +174,8 @@
<FrameLayout
android:id="@+id/contentContainer"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="-18dp"
android:elevation="1dp"
app:layout_constraintTop_toBottomOf="@id/layout_player_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
android:layout_width="0dp"
android:layout_height="0dp">
<FrameLayout
android:id="@+id/videodetail_container_main"
android:layout_width="match_parent"
@@ -394,19 +380,23 @@
android:id="@+id/videodetail_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/buttons_pins"
android:layout_marginTop="7dp"
android:layout_marginStart="15dp" />
android:layout_marginStart="15dp"
app:layout_constraintHorizontal_chainStyle="spread_inside"/>
<com.futo.platformplayer.views.pills.RoundButtonGroup
android:id="@+id/buttons_pins"
app:layout_constraintLeft_toRightOf="@id/videodetail_rating"
app:layout_constraintStart_toEndOf="@id/videodetail_rating"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="10dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="10dp"
android:layout_width="0dp"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
app:layout_constraintWidth_max="500dp"
app:layout_constraintHorizontal_chainStyle="spread_inside"/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -611,15 +601,13 @@
<FrameLayout
android:id="@+id/videodetail_quality_overview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:visibility="gone" />
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"/>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.futo.platformplayer.views.containers.CustomMotionLayout>
+1 -2
View File
@@ -7,8 +7,7 @@
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:orientation="vertical"
tools:targetApi="28">
android:orientation="vertical">
<ImageButton
android:id="@+id/button_minimize"
@@ -2,7 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:orientation="vertical">
@@ -10,7 +10,7 @@
<com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
android:id="@+id/layout_bar"
android:layout_width="match_parent"
android:layout_height="12dp"
android:layout_height="wrap_content"
app:shouldInterceptTouches="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
+4 -5
View File
@@ -7,14 +7,14 @@
android:background="@color/transparent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.media3.ui.PlayerView
android:paddingBottom="7dp"
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:default_artwork="@drawable/placeholder_video_thumbnail"
app:use_artwork="true"
app:use_controller="false"
app:show_buffering="always"
android:layout_marginBottom="6dp" />
app:show_buffering="always"/>
<!--
<androidx.media3.ui.PlayerControlView
android:id="@+id/video_player_bar"
@@ -28,8 +28,7 @@
android:id="@+id/layout_controls_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#55000000"
android:layout_marginBottom="6dp">
android:background="#55000000">
</FrameLayout>
<FrameLayout
@@ -71,4 +70,4 @@
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
+72 -312
View File
@@ -129,6 +129,19 @@
android:text=""
android:textColor="@android:color/white"
android:textSize="14sp" />
<TextView
android:id="@+id/video_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text=""
android:textColor="#CCC"
android:textSize="14sp" />
</LinearLayout>
<!-- Buttons section -->
@@ -143,341 +156,88 @@
app:layout_constraintEnd_toEndOf="parent">
<!-- Like button -->
<FrameLayout
android:id="@+id/like_container"
<com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/like_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp"
android:visibility="gone">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/like_button"
app:layout_constraintEnd_toEndOf="@id/like_button"
app:layout_constraintStart_toStartOf="@id/like_button"
app:layout_constraintTop_toTopOf="@id/like_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/like_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:checkable="true"
android:contentDescription="@string/cd_image_like_icon"
app:backgroundTint="@color/transparent"
app:icon="@drawable/thumb_up_selector"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple"
app:toggleCheckedStateOnClick="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/like_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:textColor="@android:color/white"
android:textSize="12sp" />
</FrameLayout>
android:layout_marginBottom="10dp"
android:checkable="true"
android:contentDescription="@string/cd_image_like_icon"
app:backgroundTint="@color/transparent"
app:buttonIcon_s="@drawable/ic_thumb_up_s"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple"
app:toggleCheckedStateOnClick="false" />
<!-- Dislike button -->
<FrameLayout
android:id="@+id/dislike_container"
<com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/dislike_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp"
android:visibility="gone">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/dislike_button"
app:layout_constraintEnd_toEndOf="@id/dislike_button"
app:layout_constraintStart_toStartOf="@id/dislike_button"
app:layout_constraintTop_toTopOf="@id/dislike_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dislike_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:checkable="true"
android:contentDescription="@string/cd_image_dislike_icon"
app:backgroundTint="@color/transparent"
app:icon="@drawable/thumb_down_selector"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple"
app:toggleCheckedStateOnClick="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/dislike_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:textColor="@android:color/white"
android:textSize="12sp" />
</FrameLayout>
android:layout_marginBottom="20dp"
android:checkable="true"
android:contentDescription="@string/cd_image_dislike_icon"
app:backgroundTint="@color/transparent"
app:buttonIcon_s="@drawable/ic_thumb_down_s"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:rippleColor="@color/ripple"
app:toggleCheckedStateOnClick="false" />
<!-- Comments button -->
<FrameLayout
<com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/comments_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/comments_button"
app:layout_constraintEnd_toEndOf="@id/comments_button"
app:layout_constraintStart_toStartOf="@id/comments_button"
app:layout_constraintTop_toTopOf="@id/comments_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/comments_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/comments"
app:icon="@drawable/desktop_comments"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:importantForAccessibility="no"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text="@string/comments"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</FrameLayout>
android:layout_marginBottom="20dp"
android:contentDescription="@string/comments"
app:buttonIcon_s="@drawable/ic_comment_s"
app:buttonText_s=""
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:rippleColor="@color/ripple" />
<!-- Share button -->
<FrameLayout
<com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/share_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/share_button"
app:layout_constraintEnd_toEndOf="@id/share_button"
app:layout_constraintStart_toStartOf="@id/share_button"
app:layout_constraintTop_toTopOf="@id/share_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/share_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/share"
app:icon="@drawable/desktop_share"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:importantForAccessibility="no"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text="@string/share"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</FrameLayout>
android:layout_marginBottom="20dp"
android:contentDescription="@string/share"
app:buttonIcon_s="@drawable/ic_share_s"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:rippleColor="@color/ripple" />
<!-- Refresh button -->
<FrameLayout
android:id="@+id/refresh_button_container"
<com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/refresh_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/refresh_button"
app:layout_constraintEnd_toEndOf="@id/refresh_button"
app:layout_constraintStart_toStartOf="@id/refresh_button"
app:layout_constraintTop_toTopOf="@id/refresh_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/refresh_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:contentDescription="@string/refresh"
app:icon="@drawable/desktop_refresh"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:importantForAccessibility="no"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text="@string/refresh"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</FrameLayout>
android:layout_marginBottom="20dp"
android:contentDescription="@string/refresh"
app:buttonIcon_s="@drawable/ic_refresh"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:rippleColor="@color/ripple" />
<!-- Quality/More button -->
<FrameLayout
<com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/quality_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/quality_button"
app:layout_constraintEnd_toEndOf="@id/quality_button"
app:layout_constraintStart_toStartOf="@id/quality_button"
app:layout_constraintTop_toTopOf="@id/quality_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/quality_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/quality"
app:icon="@drawable/desktop_gear"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:importantForAccessibility="no"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text="@string/quality"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</FrameLayout>
android:layout_marginBottom="10dp"
android:contentDescription="@string/quality"
app:buttonIcon_s="@drawable/ic_settings_s"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:rippleColor="@color/ripple" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -9,7 +9,7 @@
android:layout_above="@+id/short_player_progress_bar"
android:background="@color/black"
app:default_artwork="@drawable/placeholder_video_thumbnail"
app:resize_mode="fit"
app:resize_mode="fill"
app:show_buffering="when_playing"
app:use_artwork="true"
app:use_controller="false" />
@@ -17,9 +17,9 @@
<androidx.media3.ui.DefaultTimeBar
android:id="@+id/short_player_progress_bar"
android:layout_width="match_parent"
android:layout_height="6dp"
android:layout_height="3dp"
android:layout_alignParentBottom="true"
app:bar_height="6dp"
app:bar_height="3dp"
app:buffered_color="#DDEEEEEE"
app:played_color="@color/colorPrimary"
app:scrubber_disabled_size="0dp"
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="5dp"
android:orientation="vertical"
android:background="@color/transparent"
android:id="@+id/root"
android:paddingTop="3dp"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<ImageView
android:id="@+id/image_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_qr" />
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14dp"
android:autoSizeTextType="uniform"
android:fontFamily="@font/inter_light"
android:textColor="@color/white"
android:textAlignment="center"
android:maxLines="1"
android:text="" />
</LinearLayout>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ShortsButton">
<attr name="buttonIcon_s" format="reference" />
<attr name="buttonText_s" format="string" />
</declare-styleable>
</resources>
+3
View File
@@ -247,6 +247,7 @@
<string name="membership">Membership</string>
<string name="store">Store</string>
<string name="live_chat">Live Chat</string>
<string name="vod_chat">VOD Chat</string>
<string name="remove">Remove</string>
<string name="space_videos">Videos</string>
<string name="playlist">Playlist</string>
@@ -435,6 +436,8 @@
<string name="allow_full_screen_portrait">Allow full-screen portrait when watching horizontal videos</string>
<string name="delete_watchlist_on_finish">Delete from WatchLater when watched</string>
<string name="delete_watchlist_on_finish_description">After you leave a video that you mostly watched, it will be removed from watch later.</string>
<string name="shorts_pregenerate">Pre-generate shorts sources</string>
<string name="shorts_pregenerate_description">Generates short sources (when applicable) one video ahead</string>
<string name="seek_offset">Seek duration</string>
<string name="min_playback_speed">Minimum Playback Speed</string>
<string name="min_playback_speed_description">Minimum Available Speed</string>
+154 -182
View File
@@ -3,208 +3,180 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<Transition
android:id="@+id/maximize"
app:constraintSetEnd="@id/expanded"
app:constraintSetStart="@id/collapsed"
app:duration="300"
app:motionInterpolator="easeInOut">
app:duration="300">
<OnSwipe
app:dragDirection="dragUp"
app:maxAcceleration="200"
app:touchAnchorId="@+id/touchContainer"
app:nestedScrollFlags="disableScroll"
app:touchAnchorId="@id/layout_player_container"
app:touchAnchorSide="top" />
<KeyFrameSet>
<!--
<KeyAttribute
android:alpha="0"
app:framePosition="0"
app:motionTarget="@id/contentContainer" />
<KeyAttribute
android:alpha="1"
app:framePosition="100"
app:motionTarget="@id/contentContainer" /> -->
<KeyAttribute
app:framePosition="3"
app:motionTarget="@id/touchContainer">
<CustomAttribute
app:attributeName="cardElevation"
app:customDimension="0dp" />
</KeyAttribute>
<!--Minimize Progress-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeProgress"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeProgress"
app:customFloatValue="1" />
</KeyAttribute>
<!--Controller Alpha-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ControllerAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ControllerAlpha"
app:customFloatValue="1" />
</KeyAttribute>
<!--Content Alpha-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ContentAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="30"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ContentAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="ContentAlpha"
app:customFloatValue="1" />
</KeyAttribute>
<!--MinimizeControlsAlpha Alpha -->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeControlsAlpha"
app:customFloatValue="1" />
</KeyAttribute>
<KeyAttribute
app:framePosition="20"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeControlsAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="MinimizeControlsAlpha"
app:customFloatValue="0" />
</KeyAttribute>
<!--Padding Right-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="VideoMinimize"
app:customFloatValue="1" />
</KeyAttribute>
<KeyAttribute
app:framePosition="20"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="VideoMinimize"
app:customFloatValue="0" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="VideoMinimize"
app:customFloatValue="0" />
</KeyAttribute>
<!--Padding Top-->
<KeyAttribute
app:framePosition="0"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="TopPadding"
app:customDimension="1dp" />
</KeyAttribute>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@id/fragview_videodetail">
<CustomAttribute
app:attributeName="TopPadding"
app:customDimension="0dp" />
</KeyAttribute>
</KeyFrameSet>
<!--pretty sure this isn't doing anything right now-->
<OnClick
app:clickAction="transitionToEnd"
app:targetId="@id/layout_player_container" />
</Transition>
<ConstraintSet android:id="@+id/expanded">
<Transition
android:id="@+id/full_screen"
app:constraintSetEnd="@id/full_screen_gesture"
app:constraintSetStart="@id/expanded"
app:duration="300">
<Constraint
android:id="@id/touchContainer"
android:layout_width="match_parent"
android:layout_height="220dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@id/fragview_videodetail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
<OnSwipe
app:dragDirection="dragUp"
app:maxAcceleration="200"
app:nestedScrollFlags="disableScroll"
app:onTouchUp="autoCompleteToStart"
app:touchAnchorId="@id/layout_player_container"
app:touchAnchorSide="bottom" />
</Transition>
<ConstraintSet android:id="@+id/collapsed">
<Constraint
android:id="@id/touchContainer"
android:id="@id/layout_player_container"
android:layout_height="60dp"
android:layout_width="match_parent"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="48dp"
android:layout_marginBottom="47dp"
android:elevation="3dp"
android:paddingBottom="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/minimize_controls"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_weight="150"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_max="150dp" />
<Constraint
android:id="@id/contentContainer"
android:elevation="1dp"
android:orientation="vertical"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/fragview_videodetail"
android:id="@id/minimize_controls"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_width="match_parent"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="48dp"
app:layout_constraintBottom_toBottomOf="parent"
android:elevation="1dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_weight="350"
app:layout_constraintStart_toEndOf="@id/layout_player_container"
app:layout_constraintTop_toTopOf="@id/layout_player_container"
app:layout_constraintWidth_max="350dp" />
<Constraint
android:id="@id/videodetail_progress"
android:layout_height="wrap_content"
android:layout_marginTop="-12dp"
android:background="@color/black"
android:elevation="2dp"
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
app:layout_constraintStart_toStartOf="@id/layout_player_container"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/videodetail_quality_overview"
app:visibilityMode="ignore"/>
<Constraint
android:id="@id/overlay_container"
app:visibilityMode="ignore"/>
</ConstraintSet>
</MotionScene>
<ConstraintSet android:id="@+id/expanded">
<Constraint
android:id="@id/layout_player_container"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:elevation="2dp"
android:paddingBottom="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@id/contentContainer"
android:elevation="2dp"
android:orientation="vertical"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/minimize_controls"
android:layout_width="0dp"
android:layout_height="60dp"
android:elevation="1dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/layout_player_container"
app:layout_constraintTop_toTopOf="@id/layout_player_container" />
<Constraint
android:id="@id/videodetail_progress"
android:layout_height="wrap_content"
android:layout_marginTop="-12dp"
android:background="@color/transparent"
android:elevation="1dp"
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
app:layout_constraintStart_toStartOf="@id/layout_player_container"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/videodetail_quality_overview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:visibility="gone"
app:visibilityMode="ignore"/>
<Constraint
android:id="@id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:visibility="gone"
app:visibilityMode="ignore"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/full_screen_gesture">
<Constraint
android:id="@id/layout_player_container"
android:layout_height="wrap_content"
android:layout_marginTop="-130dp"
android:layout_marginBottom="0dp"
android:elevation="2dp"
android:paddingBottom="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@id/contentContainer"
android:elevation="1dp"
android:orientation="vertical"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
<Constraint
android:id="@id/minimize_controls"
android:layout_width="0dp"
android:layout_height="60dp"
android:elevation="1dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/layout_player_container"
app:layout_constraintTop_toTopOf="@id/layout_player_container" />
<Constraint
android:id="@id/videodetail_progress"
android:layout_height="wrap_content"
android:layout_marginTop="-12dp"
android:background="@color/transparent"
android:elevation="1dp"
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
app:layout_constraintStart_toStartOf="@id/layout_player_container"
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
</ConstraintSet>
</MotionScene>