Merge branch 'shorts-improv' into 'master'

Various shorts improvements, login warnings support, etc

See merge request videostreaming/grayjay!138
This commit is contained in:
Kelvin
2025-08-13 16:11:30 +00:00
39 changed files with 988 additions and 861 deletions
+2 -2
View File
@@ -154,10 +154,10 @@ android {
} }
dependencies { dependencies {
implementation 'com.google.dagger:dagger:2.48' //implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2' implementation 'androidx.test:monitor:1.7.2'
implementation 'com.google.android.material:material:1.12.0' 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 //Core
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
@@ -603,6 +603,11 @@ class Settings : FragmentedStorageFileJson() {
else -> 2.0 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) @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.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.others.LoginWebViewClient import com.futo.platformplayer.others.LoginWebViewClient
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
@@ -74,6 +75,7 @@ class LoginActivity : AppCompatActivity() {
finish(); finish();
}; };
var isFirstLoad = true; var isFirstLoad = true;
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
webViewClient.onPageLoaded.subscribe { view, url -> webViewClient.onPageLoaded.subscribe { view, url ->
_textUrl.setText(url ?: ""); _textUrl.setText(url ?: "");
@@ -86,6 +88,19 @@ class LoginActivity : AppCompatActivity() {
//TODO: Find most reliable way to wait for page js to finish //TODO: Find most reliable way to wait for page js to finish
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {}); 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; _webView.settings.domStorageEnabled = true;
@@ -1,6 +1,10 @@
package com.futo.platformplayer.api.media.platforms.js 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( class SourcePluginAuthConfig(
val loginUrl: String, val loginUrl: String,
val completionUrl: String? = null, val completionUrl: String? = null,
@@ -11,5 +15,26 @@ class SourcePluginAuthConfig(
val userAgent: String? = null, val userAgent: String? = null,
val loginButton: String? = null, val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<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.getOrThrow
import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.Language import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
@@ -57,12 +58,24 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
hasGenerate = _obj.has("generate"); 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?> { override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate) if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest)); return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed) if(_obj.isClosed)
throw IllegalStateException("Source object already closed"); 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(); val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = null; var result: V8Deferred<V8ValueString>? = null;
@@ -18,6 +18,7 @@ import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -65,11 +66,22 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
hasGenerate = _obj.has("generate"); 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?> { override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate) if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest)); return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed) if(_obj.isClosed)
throw IllegalStateException("Source object already closed"); 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(); val plugin = _plugin.getUnderlyingPlugin();
@@ -12,7 +12,7 @@ class MultiDistributionContentPager<T : IPlatformContent> : MultiPager<T> {
private val dist : HashMap<IPager<T>, Float>; private val dist : HashMap<IPager<T>, Float>;
private val distConsumed : 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(); val distTotal = pagers.values.sum();
dist = HashMap(); dist = HashMap();
@@ -719,7 +719,7 @@ class VideoDownload {
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString()); Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
var written = 0; var written: Long = 0;
var indexCounter = 0; var indexCounter = 0;
onProgress(foundCues.count().toLong(), 0, 0); onProgress(foundCues.count().toLong(), 0, 0);
for(cue in foundCues) { for(cue in foundCues) {
@@ -744,7 +744,7 @@ class VideoDownload {
indexCounter++; indexCounter++;
} }
sourceLength = written.toLong(); sourceLength = written;
Logger.i(TAG, "$name downloadSource Finished"); Logger.i(TAG, "$name downloadSource Finished");
} }
@@ -1,46 +1,27 @@
package com.futo.platformplayer.fragment.mainactivity.main package com.futo.platformplayer.fragment.mainactivity.main
import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Animatable 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.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.SoundEffectConstants
import android.view.View
import android.view.animation.AccelerateInterpolator import android.view.animation.AccelerateInterpolator
import android.view.animation.OvershootInterpolator import android.view.animation.OvershootInterpolator
import android.widget.Button
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.Format import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi 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.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs 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.ContentNotAvailableYetException
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException 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.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.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource 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.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.casting.CastConnectionState import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event3 import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.dp
import com.futo.platformplayer.engine.exceptions.ScriptAgeException import com.futo.platformplayer.engine.exceptions.ScriptAgeException
import com.futo.platformplayer.engine.exceptions.ScriptException import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fragment.mainactivity.special.CommentsModalBottomSheet
import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanBitrate import com.futo.platformplayer.toHumanBitrate
import com.futo.platformplayer.toHumanBytesSize import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.views.buttons.ShortsButton
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.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.SlideUpMenuButtonList
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem 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.overlays.slideup.SlideUpMenuTitle
import com.futo.platformplayer.views.pills.OnLikeDislikeUpdatedArgs import com.futo.platformplayer.views.pills.OnLikeDislikeUpdatedArgs
import com.futo.platformplayer.views.platform.PlatformIndicator 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.FutoShortPlayer
import com.futo.platformplayer.views.video.FutoVideoPlayerBase 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.ApiMethods
import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models import com.futo.polycentric.core.Models
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions 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.android.material.button.MaterialButton
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -116,30 +84,29 @@ import userpackage.Protocol
@UnstableApi @UnstableApi
class ShortView : FrameLayout { class ShortView : FrameLayout {
private lateinit var mainFragment: MainFragment private lateinit var fragment: MainFragment
private val player: FutoShortPlayer private val player: FutoShortPlayer
private val channelInfo: LinearLayout private val channelInfo: LinearLayout
private val creatorThumbnail: CreatorThumbnail private val creatorThumbnail: CreatorThumbnail
private val channelName: TextView private val channelName: TextView
private val videoTitle: TextView private val videoTitle: TextView
private val videoSubtitle: TextView
private val platformIndicator: PlatformIndicator private val platformIndicator: PlatformIndicator
//TODO: Replace with non-material button
private val backButton: MaterialButton private val backButton: MaterialButton
private val backButtonContainer: ConstraintLayout private val backButtonContainer: ConstraintLayout
private val likeContainer: FrameLayout private val likeButton: ShortsButton
private val dislikeContainer: FrameLayout //private val likeCount: TextView
private val likeButton: MaterialButton private val dislikeButton: ShortsButton
private val likeCount: TextView //private val dislikeCount: TextView
private val dislikeButton: MaterialButton
private val dislikeCount: TextView
private val commentsButton: MaterialButton private val commentsButton: ShortsButton
private val shareButton: MaterialButton private val shareButton: ShortsButton
private val refreshButton: MaterialButton private val refreshButton: ShortsButton
private val refreshButtonContainer: View private val qualityButton: ShortsButton
private val qualityButton: MaterialButton
private val playPauseOverlay: FrameLayout private val playPauseOverlay: FrameLayout
private val playPauseIcon: ImageView private val playPauseIcon: ImageView
@@ -173,18 +140,21 @@ class ShortView : FrameLayout {
private val onLikeDislikeUpdated = Event1<OnLikeDislikeUpdatedArgs>() private val onLikeDislikeUpdated = Event1<OnLikeDislikeUpdatedArgs>()
private val onVideoUpdated = Event1<IPlatformVideo?>() private val onVideoUpdated = Event1<IPlatformVideo?>()
//TODO: Replace with non-material UI? Only true dependency on Material left
private val bottomSheet: CommentsModalBottomSheet = CommentsModalBottomSheet() private val bottomSheet: CommentsModalBottomSheet = CommentsModalBottomSheet()
var likes: Long = 0 var likes: Long = 0
set(value) { set(value) {
field = value field = value
likeCount.text = value.toString() likeButton.withPrimaryText(value.toString());
//likeCount.text = value.toString()
} }
var dislikes: Long = 0 var dislikes: Long = 0
set(value) { set(value) {
field = 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) { constructor(inflater: LayoutInflater, fragment: MainFragment, overlayQualityContainer: FrameLayout) : this(inflater.context) {
@@ -194,7 +164,7 @@ class ShortView : FrameLayout {
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
) )
this.mainFragment = fragment this.fragment = fragment
bottomSheet.mainFragment = fragment bottomSheet.mainFragment = fragment
} }
@@ -217,19 +187,17 @@ class ShortView : FrameLayout {
creatorThumbnail = findViewById(R.id.creator_thumbnail) creatorThumbnail = findViewById(R.id.creator_thumbnail)
channelName = findViewById(R.id.channel_name) channelName = findViewById(R.id.channel_name)
videoTitle = findViewById(R.id.video_title) videoTitle = findViewById(R.id.video_title)
videoSubtitle = findViewById(R.id.video_subtitle)
platformIndicator = findViewById(R.id.short_platform_indicator) platformIndicator = findViewById(R.id.short_platform_indicator)
backButton = findViewById(R.id.back_button) backButton = findViewById(R.id.back_button)
backButtonContainer = findViewById(R.id.back_button_container) 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) likeButton = findViewById(R.id.like_button)
likeCount = findViewById(R.id.like_count) //likeCount = findViewById(R.id.like_count)
dislikeButton = findViewById(R.id.dislike_button) dislikeButton = findViewById(R.id.dislike_button)
dislikeCount = findViewById(R.id.dislike_count) //dislikeCount = findViewById(R.id.dislike_count)
commentsButton = findViewById(R.id.comments_button) commentsButton = findViewById(R.id.comments_button)
shareButton = findViewById(R.id.share_button) shareButton = findViewById(R.id.share_button)
refreshButton = findViewById(R.id.refresh_button) refreshButton = findViewById(R.id.refresh_button)
refreshButtonContainer = findViewById(R.id.refresh_button_container)
qualityButton = findViewById(R.id.quality_button) qualityButton = findViewById(R.id.quality_button)
playPauseOverlay = findViewById(R.id.play_pause_overlay) playPauseOverlay = findViewById(R.id.play_pause_overlay)
playPauseIcon = findViewById(R.id.play_pause_icon) playPauseIcon = findViewById(R.id.play_pause_icon)
@@ -258,48 +226,44 @@ class ShortView : FrameLayout {
} }
onVideoUpdated.subscribe { onVideoUpdated.subscribe {
Logger.i(TAG, "Shorts videoUpdated [${it?.name}] (isDetail: ${it is IPlatformVideoDetails}, thumbnail: ${it?.author?.thumbnail})");
videoTitle.text = it?.name videoTitle.text = it?.name
videoSubtitle.text = if(it is IPlatformVideoDetails) it?.description; else "";
platformIndicator.setPlatformFromClientID(it?.id?.pluginId) platformIndicator.setPlatformFromClientID(it?.id?.pluginId)
creatorThumbnail.setThumbnail(it?.author?.thumbnail, true) creatorThumbnail.setThumbnail(it?.author?.thumbnail, true)
channelName.text = it?.author?.name channelName.text = it?.author?.name
} }
backButton.setOnClickListener { backButton.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK) fragment.closeSegment()
mainFragment.closeSegment()
} }
channelInfo.setOnClickListener { channelInfo.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK) fragment.navigate<ChannelFragment>(video?.author)
mainFragment.navigate<ChannelFragment>(video?.author)
} }
videoTitle.setOnClickListener { videoTitle.setOnClickListener {
playSoundEffect(SoundEffectConstants.CLICK)
if (!bottomSheet.isAdded) { if (!bottomSheet.isAdded) {
bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG) bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
} }
} }
commentsButton.setOnClickListener { commentsButton.onClick.subscribe {
playSoundEffect(SoundEffectConstants.CLICK)
if (!bottomSheet.isAdded) { if (!bottomSheet.isAdded) {
bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG) bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
} }
} }
shareButton.setOnClickListener { shareButton.onClick.subscribe {
playSoundEffect(SoundEffectConstants.CLICK)
val url = video?.shareUrl ?: video?.url val url = video?.shareUrl ?: video?.url
mainFragment.startActivity(Intent.createChooser(Intent().apply { fragment.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, url) putExtra(Intent.EXTRA_TEXT, url)
type = "text/plain" type = "text/plain"
}, null)) }, null))
} }
refreshButton.setOnClickListener { refreshButton.onClick.subscribe {
playSoundEffect(SoundEffectConstants.CLICK)
onResetTriggered.emit() onResetTriggered.emit()
} }
@@ -308,14 +272,12 @@ class ShortView : FrameLayout {
false false
} }
qualityButton.setOnClickListener { qualityButton.onClick.subscribe {
playSoundEffect(SoundEffectConstants.CLICK)
showVideoSettings() showVideoSettings()
} }
likeButton.setOnClickListener { likeButton.onClick.subscribe {
playSoundEffect(SoundEffectConstants.CLICK) val checked = likeButton.iconId == R.drawable.ic_thumb_up_s // !likeButton.isChecked
val checked = !likeButton.isChecked
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
if (checked) { if (checked) {
likes++ likes++
@@ -323,24 +285,27 @@ class ShortView : FrameLayout {
likes-- 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) { if (dislikeButton.iconId == R.drawable.ic_thumb_down_s_filled && checked) {
dislikeButton.isChecked = false //dislikeButton.isChecked = false
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
dislikes-- dislikes--
} }
onLikeDislikeUpdated.emit( onLikeDislikeUpdated.emit(
OnLikeDislikeUpdatedArgs( OnLikeDislikeUpdatedArgs(
it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked it, likes, checked, dislikes, !checked
) )
) )
} }
} }
dislikeButton.setOnClickListener { dislikeButton.onClick.subscribe {
playSoundEffect(SoundEffectConstants.CLICK) val checked = dislikeButton.iconId == R.drawable.ic_thumb_down_s //!dislikeButton.isChecked
val checked = !dislikeButton.isChecked
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
if (checked) { if (checked) {
dislikes++ dislikes++
@@ -348,16 +313,21 @@ class ShortView : FrameLayout {
dislikes-- 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) { if (likeButton.iconId == R.drawable.ic_thumb_up_s_filled && checked) {
likeButton.isChecked = false //likeButton.isChecked = false
likeButton.withIcon(R.drawable.ic_thumb_up_s);
likes-- likes--
} }
onLikeDislikeUpdated.emit( onLikeDislikeUpdated.emit(
OnLikeDislikeUpdatedArgs( 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 -> onLikesLoaded.subscribe(tag) { rating, liked, disliked ->
likes = rating.likes likes = rating.likes
dislikes = rating.dislikes dislikes = rating.dislikes
likeButton.isChecked = liked //likeButton.isChecked = liked
dislikeButton.isChecked = disliked //dislikeButton.isChecked = disliked
dislikeContainer.visibility = VISIBLE dislikeButton.visibility = VISIBLE
likeContainer.visibility = VISIBLE likeButton.visibility = VISIBLE
} }
player.onPlaybackStateChanged.subscribe { player.onPlaybackStateChanged.subscribe {
@@ -565,7 +535,7 @@ class ShortView : FrameLayout {
var toSet: ISubtitleSource? = subtitleSource var toSet: ISubtitleSource? = subtitleSource
if (_lastSubtitleSource == subtitleSource) toSet = null if (_lastSubtitleSource == subtitleSource) toSet = null
mainFragment.lifecycleScope.launch(Dispatchers.Main) { fragment.lifecycleScope.launch(Dispatchers.Main) {
try { try {
player.swapSubtitles(toSet) player.swapSubtitles(toSet)
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -625,7 +595,7 @@ class ShortView : FrameLayout {
@Suppress("unused") @Suppress("unused")
fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) { fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) {
this.mainFragment = fragment this.fragment = fragment
this.bottomSheet.mainFragment = fragment this.bottomSheet.mainFragment = fragment
this.overlayQualityContainer = overlayQualityContainer this.overlayQualityContainer = overlayQualityContainer
} }
@@ -636,10 +606,10 @@ class ShortView : FrameLayout {
} }
this.video = video this.video = video
refreshButtonContainer.visibility = if (isChannelShortsMode) { refreshButton.visibility = if (isChannelShortsMode) {
GONE GONE
} else { } else {
VISIBLE GONE //TODO: Revert?
} }
backButtonContainer.visibility = if (isChannelShortsMode) { backButtonContainer.visibility = if (isChannelShortsMode) {
VISIBLE VISIBLE
@@ -695,8 +665,8 @@ class ShortView : FrameLayout {
} }
private fun loadLikes(video: IPlatformVideo) { private fun loadLikes(video: IPlatformVideo) {
likeContainer.visibility = GONE likeButton.visibility = GONE
dislikeContainer.visibility = GONE dislikeButton.visibility = GONE
loadLikesTask?.cancel() loadLikesTask?.cancel()
loadLikesTask = loadLikesTask =
@@ -735,13 +705,13 @@ class ShortView : FrameLayout {
args.processHandle.opinion(ref, Opinion.neutral) args.processHandle.opinion(ref, Opinion.neutral)
} }
mainFragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
try { try {
Logger.i(CommentsModalBottomSheet.TAG, "Started backfill") Logger.i(TAG, "Started backfill")
args.processHandle.fullyBackfillServersAnnounceExceptions() args.processHandle.fullyBackfillServersAnnounceExceptions()
Logger.i(CommentsModalBottomSheet.TAG, "Finished backfill") Logger.i(TAG, "Finished backfill")
} catch (e: Throwable) { } 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) setLoading(true)
Logger.i(TAG, "Shorts loadVideo [${url}]");
val timeLoadVideoStart = System.currentTimeMillis();
loadVideoTask = TaskHandler<String, IPlatformVideoDetails>( loadVideoTask = TaskHandler<String, IPlatformVideoDetails>(
StateApp.instance.scopeGetter, { StateApp.instance.scopeGetter, {
val result = StatePlatform.instance.getContentDetails(it).await() val result = StatePlatform.instance.getContentDetails(it).await()
if (result !is IPlatformVideoDetails) throw IllegalStateException("Expected media content, found ${result.contentType}") if (result !is IPlatformVideoDetails) throw IllegalStateException("Expected media content, found ${result.contentType}")
return@TaskHandler result return@TaskHandler result
}).success { result -> }).success { result ->
videoDetails = result val timeLoadVideo = System.currentTimeMillis() - timeLoadVideoStart;
video = result 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> { }.exception<NoPlatformClientException> {
Logger.w(TAG, "exception<NoPlatformClientException>", it) Logger.w(TAG, "exception<NoPlatformClientException>", it)
UIDialogs.showDialog( UIDialogs.showDialog(
@@ -799,7 +790,7 @@ class ShortView : FrameLayout {
UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${it.availableWhen}.", "Close") { } UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${it.availableWhen}.", "Close") { }
}.exception<ScriptImplementationException> { }.exception<ScriptImplementationException> {
Logger.w(TAG, "exception<ScriptImplementationException>", it) 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> { }.exception<ScriptAgeException> {
Logger.w(TAG, "exception<ScriptAgeException>", it) Logger.w(TAG, "exception<ScriptAgeException>", it)
UIDialogs.showDialog( UIDialogs.showDialog(
@@ -812,10 +803,10 @@ class ShortView : FrameLayout {
) )
}.exception<ScriptException> { }.exception<ScriptException> {
Logger.w(TAG, "exception<ScriptException>", it) 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> { }.exception<Throwable> {
Logger.w(ChannelFragment.TAG, "Failed to load video.", it) 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) loadVideoTask?.run(url)
@@ -849,6 +840,7 @@ class ShortView : FrameLayout {
} }
val thumbnail = videoDetails.thumbnails.getHQThumbnail() val thumbnail = videoDetails.thumbnails.getHQThumbnail()
/*
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap() if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
.load(thumbnail).into(object : CustomTarget<Bitmap>() { .load(thumbnail).into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
@@ -860,8 +852,9 @@ class ShortView : FrameLayout {
} }
}) })
else player.setArtwork(null) else player.setArtwork(null)
*/
mainFragment.lifecycleScope.launch(Dispatchers.Main) { fragment.lifecycleScope.launch(Dispatchers.Main) {
try { try {
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0) player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
if (subtitleSource != null) player.swapSubtitles(subtitleSource) if (subtitleSource != null) player.swapSubtitles(subtitleSource)
@@ -887,397 +880,4 @@ class ShortView : FrameLayout {
const val TAG = "VideoDetailView" 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.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 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.StateApp
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.system.measureTimeMillis
@UnstableApi @UnstableApi
class ShortsFragment : MainFragment() { class ShortsFragment : MainFragment() {
@@ -35,6 +39,7 @@ class ShortsFragment : MainFragment() {
private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null
private var nextPageTask: TaskHandler<ShortsFragment, List<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 var mainShortsPager: IPager<IPlatformVideo>? = null
private val mainShorts: MutableList<IPlatformVideo> = mutableListOf() private val mainShorts: MutableList<IPlatformVideo> = mutableListOf()
@@ -58,6 +63,7 @@ class ShortsFragment : MainFragment() {
private var customViewAdapter: CustomViewAdapter? = null private var customViewAdapter: CustomViewAdapter? = null
// we just completely reset the data structure so we want to tell the adapter that // 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") @SuppressLint("NotifyDataSetChanged")
override fun onShownWithView(parameter: Any?, isBack: Boolean) { override fun onShownWithView(parameter: Any?, isBack: Boolean) {
(activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails() (activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
@@ -118,7 +124,6 @@ class ShortsFragment : MainFragment() {
overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview) overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview)
sourcesButton.onClick.subscribe { sourcesButton.onClick.subscribe {
sourcesButton.playSoundEffect(SoundEffectConstants.CLICK)
navigate<SourcesFragment>() navigate<SourcesFragment>()
} }
@@ -145,7 +150,7 @@ class ShortsFragment : MainFragment() {
this.customViewAdapter = customViewAdapter this.customViewAdapter = customViewAdapter
if (loadPagerTask == null && currentShorts.isEmpty()) { if (loadPagerTask == null) {// && currentShorts.isEmpty()) {
loadPager() loadPager()
loadPagerTask!!.success { loadPagerTask!!.success {
@@ -207,28 +212,29 @@ class ShortsFragment : MainFragment() {
} }
private fun nextPage() { private fun nextPage() {
nextPageTask?.cancel() Logger.i(TAG, "ShortsFragment nextPage");
lifecycleScope.launch(Dispatchers.IO) {
val nextPageTask = try {
TaskHandler<ShortsFragment, List<IPlatformVideo>>(StateApp.instance.scopeGetter, { val time = measureTimeMillis {
currentShortsPager!!.nextPage() currentShortsPager!!.nextPage();
}
return@TaskHandler currentShortsPager!!.getResults() val newVideos = currentShortsPager!!.getResults();
}).success { newVideos ->
val prevCount = customViewAdapter!!.itemCount val prevCount = customViewAdapter!!.itemCount
Logger.i(TAG, "Shorts nextPage took ${time}ms, ${prevCount}-${prevCount + newVideos.size}, hasMore: ${currentShortsPager?.hasMorePages()}");
currentShorts.addAll(newVideos) currentShorts.addAll(newVideos)
if (isChannelShortsMode) { if (isChannelShortsMode) {
channelShorts.addAll(newVideos) channelShorts.addAll(newVideos)
} else { } else {
mainShorts.addAll(newVideos) mainShorts.addAll(newVideos)
} }
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size) lifecycleScope.launch(Dispatchers.Main) {
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
}
nextPageTask = null 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 // 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() { private fun loadPager() {
loadPagerTask?.cancel() loadPagerTask?.cancel()
Logger.i(TAG, "Shorts loadPage");
var loadPageStart = System.currentTimeMillis();
val loadPagerTask = val loadPagerTask =
TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, { TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, {
val pager = StatePlatform.instance.getShorts() val pager = StatePlatform.instance.getShorts();
return@TaskHandler pager return@TaskHandler pager
}).success { pager -> }).success { pager ->
val timeLoadPage = System.currentTimeMillis() - loadPageStart;
Logger.i(TAG, "Shorts loadPage took ${timeLoadPage}ms");
mainShorts.clear() mainShorts.clear()
mainShorts.addAll(pager.getResults()) mainShorts.addAll(pager.getResults())
mainShortsPager = pager mainShortsPager = pager
@@ -259,7 +269,7 @@ class ShortsFragment : MainFragment() {
loadPagerTask = null loadPagerTask = null
}.exception<Throwable> { err -> }.exception<Throwable> { err ->
val message = "Unable to load shorts $err" val message = "Unable to load shorts $err"
Logger.i(TAG, message) Logger.w(TAG, message, err)
if (context != null) { if (context != null) {
UIDialogs.showDialog( UIDialogs.showDialog(
requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action( requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action(
@@ -329,6 +339,7 @@ class ShortsFragment : MainFragment() {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { 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()) holder.shortView.changeVideo(videos[position], isChannelShortsMode())
if (position == itemCount - 1) { if (position == itemCount - 1) {
@@ -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"
}
}
@@ -500,7 +500,7 @@ class StatePlatform {
.toList() .toList()
.associateWith { 1f }; .associateWith { 1f };
val pager = MultiDistributionContentPager(pages); val pager = MultiDistributionContentPager(pages, 2);
pager.initialize(); pager.initialize();
return pager; return pager;
} }
@@ -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;
}
}
}
@@ -14,6 +14,7 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.getDataLinkFromUrl import com.futo.platformplayer.getDataLinkFromUrl
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.IdenticonView import com.futo.platformplayer.views.IdenticonView
import userpackage.Protocol import userpackage.Protocol
@@ -82,14 +83,14 @@ class CreatorThumbnail : ConstraintLayout {
Glide.with(_imageChannelThumbnail) Glide.with(_imageChannelThumbnail)
.load(url) .load(url)
.placeholder(R.drawable.placeholder_channel_thumbnail) .placeholder(R.drawable.placeholder_channel_thumbnail)
.diskCacheStrategy(DiskCacheStrategy.DATA) .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.crossfade() .crossfade()
.into(_imageChannelThumbnail); .into(_imageChannelThumbnail)
} else { } else {
Glide.with(_imageChannelThumbnail) Glide.with(_imageChannelThumbnail)
.load(url) .load(url)
.placeholder(R.drawable.placeholder_channel_thumbnail) .placeholder(R.drawable.placeholder_channel_thumbnail)
.diskCacheStrategy(DiskCacheStrategy.DATA) .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(_imageChannelThumbnail); .into(_imageChannelThumbnail);
} }
} }
@@ -6,6 +6,7 @@ import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import androidx.annotation.Dimension
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.PlaybackParameters import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player import androidx.media3.common.Player
@@ -65,6 +66,8 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
videoView = findViewById(R.id.short_player_view) videoView = findViewById(R.id.short_player_view)
progressBar = findViewById(R.id.short_player_progress_bar) progressBar = findViewById(R.id.short_player_progress_bar)
videoView.subtitleView?.setFixedTextSize(Dimension.SP, 18F);
if (!isInEditMode) { if (!isInEditMode) {
player = StatePlayer.instance.getShortPlayerOrCreate(context) player = StatePlayer.instance.getShortPlayerOrCreate(context)
player.player.repeatMode = Player.REPEAT_MODE_ONE player.player.repeatMode = Player.REPEAT_MODE_ONE
@@ -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>
+72 -312
View File
@@ -129,6 +129,19 @@
android:text="" android:text=""
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" /> 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> </LinearLayout>
<!-- Buttons section --> <!-- Buttons section -->
@@ -143,341 +156,88 @@
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<!-- Like button --> <!-- Like button -->
<FrameLayout <com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/like_container" android:id="@+id/like_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_marginBottom="10dp"
android:layout_marginBottom="12dp" android:checkable="true"
android:visibility="gone"> android:contentDescription="@string/cd_image_like_icon"
app:backgroundTint="@color/transparent"
<androidx.constraintlayout.widget.ConstraintLayout app:buttonIcon_s="@drawable/ic_thumb_up_s"
android:layout_width="wrap_content" app:iconSize="24dp"
android:layout_height="wrap_content" app:iconTint="@android:color/white"
android:layout_gravity="center_horizontal"> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
<ImageView app:layout_constraintStart_toStartOf="parent"
android:layout_width="0dp" app:layout_constraintTop_toTopOf="parent"
android:layout_height="0dp" app:rippleColor="@color/ripple"
android:importantForAccessibility="no" app:toggleCheckedStateOnClick="false" />
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>
<!-- Dislike button --> <!-- Dislike button -->
<FrameLayout <com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/dislike_container" android:id="@+id/dislike_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_marginBottom="20dp"
android:layout_marginBottom="12dp" android:checkable="true"
android:visibility="gone"> android:contentDescription="@string/cd_image_dislike_icon"
app:backgroundTint="@color/transparent"
<androidx.constraintlayout.widget.ConstraintLayout app:buttonIcon_s="@drawable/ic_thumb_down_s"
android:layout_width="wrap_content" app:iconSize="24dp"
android:layout_height="wrap_content" app:iconTint="@android:color/white"
android:layout_gravity="center_horizontal"> app:rippleColor="@color/ripple"
app:toggleCheckedStateOnClick="false" />
<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>
<!-- Comments button --> <!-- Comments button -->
<FrameLayout <com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/comments_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_marginBottom="20dp"
android:layout_marginBottom="12dp"> android:contentDescription="@string/comments"
app:buttonIcon_s="@drawable/ic_comment_s"
<androidx.constraintlayout.widget.ConstraintLayout app:buttonText_s=""
android:layout_width="wrap_content" app:iconSize="24dp"
android:layout_height="wrap_content" app:iconTint="@android:color/white"
android:layout_gravity="center_horizontal"> app:rippleColor="@color/ripple" />
<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>
<!-- Share button --> <!-- Share button -->
<FrameLayout <com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/share_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_marginBottom="20dp"
android:layout_marginBottom="12dp"> android:contentDescription="@string/share"
app:buttonIcon_s="@drawable/ic_share_s"
<androidx.constraintlayout.widget.ConstraintLayout app:iconSize="24dp"
android:layout_width="wrap_content" app:iconTint="@android:color/white"
android:layout_height="wrap_content" app:rippleColor="@color/ripple" />
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>
<!-- Refresh button --> <!-- Refresh button -->
<FrameLayout <com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/refresh_button_container" android:id="@+id/refresh_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp"> android:layout_marginBottom="20dp"
android:contentDescription="@string/refresh"
<androidx.constraintlayout.widget.ConstraintLayout app:buttonIcon_s="@drawable/ic_refresh"
android:layout_width="wrap_content" app:iconSize="24dp"
android:layout_height="wrap_content" app:iconTint="@android:color/white"
android:layout_gravity="center_horizontal"> app:rippleColor="@color/ripple" />
<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>
<!-- Quality/More button --> <!-- Quality/More button -->
<FrameLayout <com.futo.platformplayer.views.buttons.ShortsButton
android:id="@+id/quality_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"> android:layout_marginBottom="10dp"
android:contentDescription="@string/quality"
<androidx.constraintlayout.widget.ConstraintLayout app:buttonIcon_s="@drawable/ic_settings_s"
android:layout_width="wrap_content" app:iconSize="24dp"
android:layout_height="wrap_content" app:iconTint="@android:color/white"
android:layout_gravity="center_horizontal"> app:rippleColor="@color/ripple" />
<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>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
@@ -9,7 +9,7 @@
android:layout_above="@+id/short_player_progress_bar" android:layout_above="@+id/short_player_progress_bar"
android:background="@color/black" android:background="@color/black"
app:default_artwork="@drawable/placeholder_video_thumbnail" app:default_artwork="@drawable/placeholder_video_thumbnail"
app:resize_mode="fit" app:resize_mode="fill"
app:show_buffering="when_playing" app:show_buffering="when_playing"
app:use_artwork="true" app:use_artwork="true"
app:use_controller="false" /> app:use_controller="false" />
@@ -17,9 +17,9 @@
<androidx.media3.ui.DefaultTimeBar <androidx.media3.ui.DefaultTimeBar
android:id="@+id/short_player_progress_bar" android:id="@+id/short_player_progress_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="6dp" android:layout_height="3dp"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
app:bar_height="6dp" app:bar_height="3dp"
app:buffered_color="#DDEEEEEE" app:buffered_color="#DDEEEEEE"
app:played_color="@color/colorPrimary" app:played_color="@color/colorPrimary"
app:scrubber_disabled_size="0dp" 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>
+2
View File
@@ -435,6 +435,8 @@
<string name="allow_full_screen_portrait">Allow full-screen portrait when watching horizontal videos</string> <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">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="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="seek_offset">Seek duration</string>
<string name="min_playback_speed">Minimum Playback Speed</string> <string name="min_playback_speed">Minimum Playback Speed</string>
<string name="min_playback_speed_description">Minimum Available Speed</string> <string name="min_playback_speed_description">Minimum Available Speed</string>