diff --git a/app/build.gradle b/app/build.gradle index 25d458d4..65f600c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,10 +154,10 @@ android { } dependencies { - implementation 'com.google.dagger:dagger:2.48' + //implementation 'com.google.dagger:dagger:2.48' implementation 'androidx.test:monitor:1.7.2' implementation 'com.google.android.material:material:1.12.0' - annotationProcessor 'com.google.dagger:dagger-compiler:2.48' + //annotationProcessor 'com.google.dagger:dagger-compiler:2.48' //Core implementation 'androidx.core:core-ktx:1.12.0' diff --git a/app/src/main/java/com/futo/platformplayer/activities/LoginActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/LoginActivity.kt index 6ea7bd67..1dc4376e 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/LoginActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/LoginActivity.kt @@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.others.LoginWebViewClient import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp @@ -74,6 +75,7 @@ class LoginActivity : AppCompatActivity() { finish(); }; var isFirstLoad = true; + val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf(); webViewClient.onPageLoaded.subscribe { view, url -> _textUrl.setText(url ?: ""); @@ -86,6 +88,19 @@ class LoginActivity : AppCompatActivity() { //TODO: Find most reliable way to wait for page js to finish view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {}); } + + if(loginWarnings.size > 0) { + synchronized(loginWarnings) { + val warning = loginWarnings.find { it.url.matches(it.getRegex()) }; + if(warning != null) { + if(warning.once == true) + loginWarnings.remove(warning); + UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0, + UIDialogs.Action("Understood", { + }, UIDialogs.ActionStyle.PRIMARY)); + } + } + } } _webView.settings.domStorageEnabled = true; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginAuthConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginAuthConfig.kt index 439e8d82..8821c79e 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginAuthConfig.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginAuthConfig.kt @@ -1,6 +1,10 @@ package com.futo.platformplayer.api.media.platforms.js -@kotlinx.serialization.Serializable +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import java.util.Dictionary + +@Serializable class SourcePluginAuthConfig( val loginUrl: String, val completionUrl: String? = null, @@ -11,5 +15,26 @@ class SourcePluginAuthConfig( val userAgent: String? = null, val loginButton: String? = null, val domainHeadersToFind: Map>? = null, - val loginWarning: String? = null -) { } \ No newline at end of file + val loginWarning: String? = null, + val loginWarnings: List? = 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; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/structures/MultiDistributionContentPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/structures/MultiDistributionContentPager.kt index 66554572..4380fea0 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/structures/MultiDistributionContentPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/structures/MultiDistributionContentPager.kt @@ -12,7 +12,7 @@ class MultiDistributionContentPager : MultiPager { private val dist : HashMap, Float>; private val distConsumed : HashMap, Float>; - constructor(pagers : Map, Float>) : super(pagers.keys.toMutableList()) { + constructor(pagers : Map, Float>, pageSize: Int = 9) : super(pagers.keys.toMutableList(), false, pageSize) { val distTotal = pagers.values.sum(); dist = HashMap(); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index 478c2732..2ec3d993 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -1,46 +1,27 @@ package com.futo.platformplayer.fragment.mainactivity.main -import android.app.Dialog import android.content.Context -import android.content.DialogInterface import android.content.Intent -import android.graphics.Bitmap import android.graphics.drawable.Animatable -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.text.Spanned import android.util.AttributeSet -import android.util.TypedValue import android.view.LayoutInflater -import android.view.SoundEffectConstants -import android.view.View import android.view.animation.AccelerateInterpolator import android.view.animation.OvershootInterpolator -import android.widget.Button import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.graphics.drawable.toDrawable -import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import androidx.media3.common.C import androidx.media3.common.Format import androidx.media3.common.util.UnstableApi -import com.bumptech.glide.Glide -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs -import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException -import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink -import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes -import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource @@ -54,40 +35,28 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig -import com.futo.platformplayer.casting.CastConnectionState -import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event3 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.downloads.VideoLocal -import com.futo.platformplayer.dp import com.futo.platformplayer.engine.exceptions.ScriptAgeException import com.futo.platformplayer.engine.exceptions.ScriptException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.exceptions.UnsupportedCastException -import com.futo.platformplayer.fixHtmlLinks -import com.futo.platformplayer.getNowDiffSeconds +import com.futo.platformplayer.fragment.mainactivity.special.CommentsModalBottomSheet import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp -import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.toHumanBitrate import com.futo.platformplayer.toHumanBytesSize -import com.futo.platformplayer.toHumanNowDiffString -import com.futo.platformplayer.toHumanNumber -import com.futo.platformplayer.views.MonetizationView -import com.futo.platformplayer.views.comments.AddCommentView +import com.futo.platformplayer.views.buttons.ShortsButton import com.futo.platformplayer.views.others.CreatorThumbnail -import com.futo.platformplayer.views.overlays.DescriptionOverlay -import com.futo.platformplayer.views.overlays.RepliesOverlay -import com.futo.platformplayer.views.overlays.SupportOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem @@ -95,20 +64,15 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTitle import com.futo.platformplayer.views.pills.OnLikeDislikeUpdatedArgs import com.futo.platformplayer.views.platform.PlatformIndicator -import com.futo.platformplayer.views.segments.CommentsList import com.futo.platformplayer.views.video.FutoShortPlayer import com.futo.platformplayer.views.video.FutoVideoPlayerBase import com.futo.polycentric.core.ApiMethods import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.Models import com.futo.polycentric.core.Opinion -import com.futo.polycentric.core.PolycentricProfile import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions -import com.futo.polycentric.core.toURLInfoSystemLinkUrl -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton +//import com.google.android.material.button.MaterialButton import com.google.protobuf.ByteString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -116,30 +80,29 @@ import userpackage.Protocol @UnstableApi class ShortView : FrameLayout { - private lateinit var mainFragment: MainFragment + private lateinit var fragment: MainFragment private val player: FutoShortPlayer private val channelInfo: LinearLayout private val creatorThumbnail: CreatorThumbnail private val channelName: TextView private val videoTitle: TextView + private val videoSubtitle: TextView private val platformIndicator: PlatformIndicator + //TODO: Replace with non-material button private val backButton: MaterialButton private val backButtonContainer: ConstraintLayout - private val likeContainer: FrameLayout - private val dislikeContainer: FrameLayout - private val likeButton: MaterialButton - private val likeCount: TextView - private val dislikeButton: MaterialButton - private val dislikeCount: TextView + private val likeButton: ShortsButton + //private val likeCount: TextView + private val dislikeButton: ShortsButton + //private val dislikeCount: TextView - private val commentsButton: MaterialButton - private val shareButton: MaterialButton - private val refreshButton: MaterialButton - private val refreshButtonContainer: View - private val qualityButton: MaterialButton + private val commentsButton: ShortsButton + private val shareButton: ShortsButton + private val refreshButton: ShortsButton + private val qualityButton: ShortsButton private val playPauseOverlay: FrameLayout private val playPauseIcon: ImageView @@ -173,18 +136,21 @@ class ShortView : FrameLayout { private val onLikeDislikeUpdated = Event1() private val onVideoUpdated = Event1() + //TODO: Replace with non-material UI? Only true dependency on Material left private val bottomSheet: CommentsModalBottomSheet = CommentsModalBottomSheet() var likes: Long = 0 set(value) { field = value - likeCount.text = value.toString() + likeButton.withPrimaryText(value.toString()); + //likeCount.text = value.toString() } var dislikes: Long = 0 set(value) { field = value - dislikeCount.text = value.toString() + dislikeButton.withPrimaryText(value.toString()); + //dislikeCount.text = value.toString() } constructor(inflater: LayoutInflater, fragment: MainFragment, overlayQualityContainer: FrameLayout) : this(inflater.context) { @@ -194,7 +160,7 @@ class ShortView : FrameLayout { LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT ) - this.mainFragment = fragment + this.fragment = fragment bottomSheet.mainFragment = fragment } @@ -217,19 +183,17 @@ class ShortView : FrameLayout { creatorThumbnail = findViewById(R.id.creator_thumbnail) channelName = findViewById(R.id.channel_name) videoTitle = findViewById(R.id.video_title) + videoSubtitle = findViewById(R.id.video_subtitle) platformIndicator = findViewById(R.id.short_platform_indicator) backButton = findViewById(R.id.back_button) backButtonContainer = findViewById(R.id.back_button_container) - likeContainer = findViewById(R.id.like_container) - dislikeContainer = findViewById(R.id.dislike_container) likeButton = findViewById(R.id.like_button) - likeCount = findViewById(R.id.like_count) + //likeCount = findViewById(R.id.like_count) dislikeButton = findViewById(R.id.dislike_button) - dislikeCount = findViewById(R.id.dislike_count) + //dislikeCount = findViewById(R.id.dislike_count) commentsButton = findViewById(R.id.comments_button) shareButton = findViewById(R.id.share_button) refreshButton = findViewById(R.id.refresh_button) - refreshButtonContainer = findViewById(R.id.refresh_button_container) qualityButton = findViewById(R.id.quality_button) playPauseOverlay = findViewById(R.id.play_pause_overlay) playPauseIcon = findViewById(R.id.play_pause_icon) @@ -258,48 +222,44 @@ class ShortView : FrameLayout { } onVideoUpdated.subscribe { + Logger.i(TAG, "Shorts videoUpdated [${it?.name}] (isDetail: ${it is IPlatformVideoDetails}, thumbnail: ${it?.author?.thumbnail})"); videoTitle.text = it?.name + videoSubtitle.text = if(it is IPlatformVideoDetails) it?.description; else ""; platformIndicator.setPlatformFromClientID(it?.id?.pluginId) creatorThumbnail.setThumbnail(it?.author?.thumbnail, true) channelName.text = it?.author?.name } backButton.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) - mainFragment.closeSegment() + fragment.closeSegment() } channelInfo.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) - mainFragment.navigate(video?.author) + fragment.navigate(video?.author) } videoTitle.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) if (!bottomSheet.isAdded) { - bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG) + bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG) } } - commentsButton.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) + commentsButton.onClick.subscribe { if (!bottomSheet.isAdded) { - bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG) + bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG) } } - shareButton.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) + shareButton.onClick.subscribe { val url = video?.shareUrl ?: video?.url - mainFragment.startActivity(Intent.createChooser(Intent().apply { + fragment.startActivity(Intent.createChooser(Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, url) type = "text/plain" }, null)) } - refreshButton.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) + refreshButton.onClick.subscribe { onResetTriggered.emit() } @@ -308,14 +268,12 @@ class ShortView : FrameLayout { false } - qualityButton.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) + qualityButton.onClick.subscribe { showVideoSettings() } - likeButton.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) - val checked = !likeButton.isChecked + likeButton.onClick.subscribe { + val checked = likeButton.iconId == R.drawable.ic_thumb_up_s // !likeButton.isChecked StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { if (checked) { likes++ @@ -323,24 +281,27 @@ class ShortView : FrameLayout { likes-- } - likeButton.isChecked = checked + if(checked) + likeButton.withIcon(R.drawable.ic_thumb_up_s_filled) //.isChecked = checked + else + likeButton.withIcon(R.drawable.ic_thumb_up_s) - if (dislikeButton.isChecked && checked) { - dislikeButton.isChecked = false + if (dislikeButton.iconId == R.drawable.ic_thumb_down_s_filled && checked) { + //dislikeButton.isChecked = false + dislikeButton.withIcon(R.drawable.ic_thumb_down_s) dislikes-- } onLikeDislikeUpdated.emit( OnLikeDislikeUpdatedArgs( - it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked + it, likes, checked, dislikes, !checked ) ) } } - dislikeButton.setOnClickListener { - playSoundEffect(SoundEffectConstants.CLICK) - val checked = !dislikeButton.isChecked + dislikeButton.onClick.subscribe { + val checked = dislikeButton.iconId == R.drawable.ic_thumb_down_s //!dislikeButton.isChecked StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { if (checked) { dislikes++ @@ -348,16 +309,21 @@ class ShortView : FrameLayout { dislikes-- } - dislikeButton.isChecked = checked + //dislikeButton.isChecked = checked + if(checked) + dislikeButton.withIcon(R.drawable.ic_thumb_down_s_filled) //.isChecked = checked + else + dislikeButton.withIcon(R.drawable.ic_thumb_down_s) - if (likeButton.isChecked && checked) { - likeButton.isChecked = false + if (likeButton.iconId == R.drawable.ic_thumb_up_s_filled && checked) { + //likeButton.isChecked = false + likeButton.withIcon(R.drawable.ic_thumb_up_s); likes-- } onLikeDislikeUpdated.emit( OnLikeDislikeUpdatedArgs( - it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked + it, likes, !checked, dislikes, checked ) ) } @@ -366,11 +332,11 @@ class ShortView : FrameLayout { onLikesLoaded.subscribe(tag) { rating, liked, disliked -> likes = rating.likes dislikes = rating.dislikes - likeButton.isChecked = liked - dislikeButton.isChecked = disliked + //likeButton.isChecked = liked + //dislikeButton.isChecked = disliked - dislikeContainer.visibility = VISIBLE - likeContainer.visibility = VISIBLE + dislikeButton.visibility = VISIBLE + likeButton.visibility = VISIBLE } player.onPlaybackStateChanged.subscribe { @@ -565,7 +531,7 @@ class ShortView : FrameLayout { var toSet: ISubtitleSource? = subtitleSource if (_lastSubtitleSource == subtitleSource) toSet = null - mainFragment.lifecycleScope.launch(Dispatchers.Main) { + fragment.lifecycleScope.launch(Dispatchers.Main) { try { player.swapSubtitles(toSet) } catch (e: Throwable) { @@ -625,7 +591,7 @@ class ShortView : FrameLayout { @Suppress("unused") fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) { - this.mainFragment = fragment + this.fragment = fragment this.bottomSheet.mainFragment = fragment this.overlayQualityContainer = overlayQualityContainer } @@ -636,10 +602,10 @@ class ShortView : FrameLayout { } this.video = video - refreshButtonContainer.visibility = if (isChannelShortsMode) { + refreshButton.visibility = if (isChannelShortsMode) { GONE } else { - VISIBLE + GONE //TODO: Revert? } backButtonContainer.visibility = if (isChannelShortsMode) { VISIBLE @@ -695,8 +661,8 @@ class ShortView : FrameLayout { } private fun loadLikes(video: IPlatformVideo) { - likeContainer.visibility = GONE - dislikeContainer.visibility = GONE + likeButton.visibility = GONE + dislikeButton.visibility = GONE loadLikesTask?.cancel() loadLikesTask = @@ -735,13 +701,13 @@ class ShortView : FrameLayout { args.processHandle.opinion(ref, Opinion.neutral) } - mainFragment.lifecycleScope.launch(Dispatchers.IO) { + fragment.lifecycleScope.launch(Dispatchers.IO) { try { - Logger.i(CommentsModalBottomSheet.TAG, "Started backfill") + Logger.i(TAG, "Started backfill") args.processHandle.fullyBackfillServersAnnounceExceptions() - Logger.i(CommentsModalBottomSheet.TAG, "Finished backfill") + Logger.i(TAG, "Finished backfill") } catch (e: Throwable) { - Logger.e(CommentsModalBottomSheet.TAG, "Failed to backfill servers", e) + Logger.e(TAG, "Failed to backfill servers", e) } } @@ -763,20 +729,24 @@ class ShortView : FrameLayout { setLoading(true) + Logger.i(TAG, "Shorts loadVideo [${url}]"); + val timeLoadVideoStart = System.currentTimeMillis(); loadVideoTask = TaskHandler( StateApp.instance.scopeGetter, { val result = StatePlatform.instance.getContentDetails(it).await() if (result !is IPlatformVideoDetails) throw IllegalStateException("Expected media content, found ${result.contentType}") return@TaskHandler result }).success { result -> - videoDetails = result - video = result + val timeLoadVideo = System.currentTimeMillis() - timeLoadVideoStart; + Logger.i(TAG, "Shorts loadVideo [${url}] took ${timeLoadVideo}ms"); + videoDetails = result + video = result - bottomSheet.video = result + bottomSheet.video = result - setLoading(false) + setLoading(false) - if (playWhenReady) playVideo() + if (playWhenReady) playVideo() }.exception { Logger.w(TAG, "exception", it) UIDialogs.showDialog( @@ -799,7 +769,7 @@ class ShortView : FrameLayout { UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${it.availableWhen}.", "Close") { } }.exception { Logger.w(TAG, "exception", 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 { Logger.w(TAG, "exception", it) UIDialogs.showDialog( @@ -812,10 +782,10 @@ class ShortView : FrameLayout { ) }.exception { Logger.w(TAG, "exception", 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 { Logger.w(ChannelFragment.TAG, "Failed to load video.", it) - UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, mainFragment) + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, fragment) } loadVideoTask?.run(url) @@ -849,6 +819,7 @@ class ShortView : FrameLayout { } val thumbnail = videoDetails.thumbnails.getHQThumbnail() + /* if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap() .load(thumbnail).into(object : CustomTarget() { override fun onResourceReady(resource: Bitmap, transition: Transition?) { @@ -860,8 +831,9 @@ class ShortView : FrameLayout { } }) else player.setArtwork(null) + */ - mainFragment.lifecycleScope.launch(Dispatchers.Main) { + fragment.lifecycleScope.launch(Dispatchers.Main) { try { player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0) if (subtitleSource != null) player.swapSubtitles(subtitleSource) @@ -887,397 +859,4 @@ class ShortView : FrameLayout { const val TAG = "VideoDetailView" } - class CommentsModalBottomSheet : BottomSheetDialogFragment() { - var mainFragment: MainFragment? = null - - private lateinit var containerContent: FrameLayout - private lateinit var containerContentMain: LinearLayout - private lateinit var containerContentReplies: RepliesOverlay - private lateinit var containerContentDescription: DescriptionOverlay - private lateinit var containerContentSupport: SupportOverlay - - private lateinit var title: TextView - private lateinit var subTitle: TextView - private lateinit var channelName: TextView - private lateinit var channelMeta: TextView - private lateinit var creatorThumbnail: CreatorThumbnail - private lateinit var channelButton: LinearLayout - private lateinit var monetization: MonetizationView - private lateinit var platform: PlatformIndicator - private lateinit var textLikes: TextView - private lateinit var textDislikes: TextView - private lateinit var layoutRating: LinearLayout - private lateinit var imageDislikeIcon: ImageView - private lateinit var imageLikeIcon: ImageView - - private lateinit var description: TextView - private lateinit var descriptionContainer: LinearLayout - private lateinit var descriptionViewMore: TextView - - private lateinit var commentsList: CommentsList - private lateinit var addCommentView: AddCommentView - - private var polycentricProfile: PolycentricProfile? = null - - private lateinit var buttonPolycentric: Button - private lateinit var buttonPlatform: Button - - private var tabIndex: Int? = null - - private var contentOverlayView: View? = null - - lateinit var video: IPlatformVideoDetails - - private lateinit var behavior: BottomSheetBehavior - - private val _taskLoadPolycentricProfile = - TaskHandler(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 { - 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 = 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(it) - } - - addCommentView.onCommentAdded.subscribe { - commentsList.addComment(it) - } - - channelButton.setOnClickListener { - mainFragment!!.navigate(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" - } - } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt index f082c91d..61e91199 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt @@ -11,6 +11,7 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import androidx.annotation.OptIn +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 @@ -25,6 +26,9 @@ import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.views.buttons.BigButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlin.system.measureTimeMillis @UnstableApi class ShortsFragment : MainFragment() { @@ -35,6 +39,7 @@ class ShortsFragment : MainFragment() { private var loadPagerTask: TaskHandler>? = null private var nextPageTask: TaskHandler>? = null + //TODO: Reduce number of pagers (1, or at most 2) private var mainShortsPager: IPager? = null private val mainShorts: MutableList = mutableListOf() @@ -58,6 +63,7 @@ class ShortsFragment : MainFragment() { private var customViewAdapter: CustomViewAdapter? = null // we just completely reset the data structure so we want to tell the adapter that + //TODO: Move most of this logic to ShortsView @SuppressLint("NotifyDataSetChanged") override fun onShownWithView(parameter: Any?, isBack: Boolean) { (activity as MainActivity?)?.getFragment()?.closeVideoDetails() @@ -118,7 +124,6 @@ class ShortsFragment : MainFragment() { overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview) sourcesButton.onClick.subscribe { - sourcesButton.playSoundEffect(SoundEffectConstants.CLICK) navigate() } @@ -145,7 +150,7 @@ class ShortsFragment : MainFragment() { this.customViewAdapter = customViewAdapter - if (loadPagerTask == null && currentShorts.isEmpty()) { + if (loadPagerTask == null) {// && currentShorts.isEmpty()) { loadPager() loadPagerTask!!.success { @@ -207,28 +212,29 @@ class ShortsFragment : MainFragment() { } private fun nextPage() { - nextPageTask?.cancel() - - val nextPageTask = - TaskHandler>(StateApp.instance.scopeGetter, { - currentShortsPager!!.nextPage() - - return@TaskHandler currentShortsPager!!.getResults() - }).success { newVideos -> + Logger.i(TAG, "ShortsFragment nextPage"); + lifecycleScope.launch(Dispatchers.IO) { + try { + val time = measureTimeMillis { + currentShortsPager!!.nextPage(); + } + val newVideos = currentShortsPager!!.getResults(); val prevCount = customViewAdapter!!.itemCount + Logger.i(TAG, "Shorts nextPage took ${time}ms, ${prevCount}-${prevCount + newVideos.size}, hasMore: ${currentShortsPager?.hasMorePages()}"); currentShorts.addAll(newVideos) if (isChannelShortsMode) { channelShorts.addAll(newVideos) } else { mainShorts.addAll(newVideos) } - customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size) + lifecycleScope.launch(Dispatchers.Main) { + customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size) + } nextPageTask = null + } catch (ex: Throwable) { + Logger.e(TAG, "Shorts Failed to call nextPage", ex); } - - nextPageTask.run(this) - - this.nextPageTask = nextPageTask + } } // we just completely reset the data structure so we want to tell the adapter that @@ -236,12 +242,16 @@ class ShortsFragment : MainFragment() { private fun loadPager() { loadPagerTask?.cancel() + Logger.i(TAG, "Shorts loadPage"); + var loadPageStart = System.currentTimeMillis(); val loadPagerTask = TaskHandler>(StateApp.instance.scopeGetter, { - val pager = StatePlatform.instance.getShorts() + val pager = StatePlatform.instance.getShorts(); return@TaskHandler pager }).success { pager -> + val timeLoadPage = System.currentTimeMillis() - loadPageStart; + Logger.i(TAG, "Shorts loadPage took ${timeLoadPage}ms"); mainShorts.clear() mainShorts.addAll(pager.getResults()) mainShortsPager = pager @@ -259,7 +269,7 @@ class ShortsFragment : MainFragment() { loadPagerTask = null }.exception { err -> val message = "Unable to load shorts $err" - Logger.i(TAG, message) + Logger.w(TAG, message, err) if (context != null) { UIDialogs.showDialog( requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action( @@ -329,6 +339,7 @@ class ShortsFragment : MainFragment() { @OptIn(UnstableApi::class) override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { + Logger.i(TAG, "Shorts change (position: ${position}): ${videos[position].name} (${videos[position].id.value})") holder.shortView.changeVideo(videos[position], isChannelShortsMode()) if (position == itemCount - 1) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/special/CommentsModalBottomSheet.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/special/CommentsModalBottomSheet.kt new file mode 100644 index 00000000..c74a7218 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/special/CommentsModalBottomSheet.kt @@ -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 + + private val _taskLoadPolycentricProfile = + TaskHandler(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 { + 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 = 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(it) + } + + addCommentView.onCommentAdded.subscribe { + commentsList.addComment(it) + } + + channelButton.setOnClickListener { + mainFragment!!.navigate(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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index cba656dd..972ce336 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -500,7 +500,7 @@ class StatePlatform { .toList() .associateWith { 1f }; - val pager = MultiDistributionContentPager(pages); + val pager = MultiDistributionContentPager(pages, 2); pager.initialize(); return pager; } diff --git a/app/src/main/java/com/futo/platformplayer/views/buttons/ShortsButton.kt b/app/src/main/java/com/futo/platformplayer/views/buttons/ShortsButton.kt new file mode 100644 index 00000000..68ba715b --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/buttons/ShortsButton.kt @@ -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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt b/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt index d655d7dd..9ea6819f 100644 --- a/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt +++ b/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt @@ -14,6 +14,7 @@ import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.getDataLinkFromUrl import com.futo.platformplayer.images.GlideHelper.Companion.crossfade +import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.views.IdenticonView import userpackage.Protocol @@ -82,14 +83,14 @@ class CreatorThumbnail : ConstraintLayout { Glide.with(_imageChannelThumbnail) .load(url) .placeholder(R.drawable.placeholder_channel_thumbnail) - .diskCacheStrategy(DiskCacheStrategy.DATA) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .crossfade() - .into(_imageChannelThumbnail); + .into(_imageChannelThumbnail) } else { Glide.with(_imageChannelThumbnail) .load(url) .placeholder(R.drawable.placeholder_channel_thumbnail) - .diskCacheStrategy(DiskCacheStrategy.DATA) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .into(_imageChannelThumbnail); } } diff --git a/app/src/main/res/drawable/ic_comment_s.xml b/app/src/main/res/drawable/ic_comment_s.xml new file mode 100644 index 00000000..6fdc655f --- /dev/null +++ b/app/src/main/res/drawable/ic_comment_s.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_s.xml b/app/src/main/res/drawable/ic_settings_s.xml new file mode 100644 index 00000000..0ef3317e --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_s.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_s.xml b/app/src/main/res/drawable/ic_share_s.xml new file mode 100644 index 00000000..9d814197 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_s.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_thumb_down_s.xml b/app/src/main/res/drawable/ic_thumb_down_s.xml new file mode 100644 index 00000000..aa4228fa --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_down_s.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_thumb_down_s_filled.xml b/app/src/main/res/drawable/ic_thumb_down_s_filled.xml new file mode 100644 index 00000000..96f55911 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_down_s_filled.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_thumb_up_s.xml b/app/src/main/res/drawable/ic_thumb_up_s.xml new file mode 100644 index 00000000..2a0d62e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_up_s.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_thumb_up_s_filled.xml b/app/src/main/res/drawable/ic_thumb_up_s_filled.xml new file mode 100644 index 00000000..a029aca4 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_up_s_filled.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/view_short_overlay.xml b/app/src/main/res/layout/view_short_overlay.xml index b55e49e0..3d961c6b 100644 --- a/app/src/main/res/layout/view_short_overlay.xml +++ b/app/src/main/res/layout/view_short_overlay.xml @@ -129,6 +129,19 @@ android:text="" android:textColor="@android:color/white" android:textSize="14sp" /> + @@ -143,341 +156,88 @@ app:layout_constraintEnd_toEndOf="parent"> - - - - - - - - - - - + android:layout_marginBottom="10dp" + android:checkable="true" + android:contentDescription="@string/cd_image_like_icon" + app:backgroundTint="@color/transparent" + app:buttonIcon_s="@drawable/ic_thumb_up_s" + app:iconSize="24dp" + app:iconTint="@android:color/white" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:rippleColor="@color/ripple" + app:toggleCheckedStateOnClick="false" /> - - - - - - - - - - - + android:layout_marginBottom="20dp" + android:checkable="true" + android:contentDescription="@string/cd_image_dislike_icon" + app:backgroundTint="@color/transparent" + app:buttonIcon_s="@drawable/ic_thumb_down_s" + app:iconSize="24dp" + app:iconTint="@android:color/white" + app:rippleColor="@color/ripple" + app:toggleCheckedStateOnClick="false" /> - - - - - - - - - - - + android:layout_marginBottom="20dp" + android:contentDescription="@string/comments" + app:buttonIcon_s="@drawable/ic_comment_s" + app:buttonText_s="" + app:iconSize="24dp" + app:iconTint="@android:color/white" + app:rippleColor="@color/ripple" /> - - - - - - - - - - - + android:layout_marginBottom="20dp" + android:contentDescription="@string/share" + app:buttonIcon_s="@drawable/ic_share_s" + app:iconSize="24dp" + app:iconTint="@android:color/white" + app:rippleColor="@color/ripple" /> - - - - - - - - - - - + android:layout_marginBottom="20dp" + android:contentDescription="@string/refresh" + app:buttonIcon_s="@drawable/ic_refresh" + app:iconSize="24dp" + app:iconTint="@android:color/white" + app:rippleColor="@color/ripple" /> - - - - - - - - - - - + android:layout_marginBottom="10dp" + android:contentDescription="@string/quality" + app:buttonIcon_s="@drawable/ic_settings_s" + app:iconSize="24dp" + app:iconTint="@android:color/white" + app:rippleColor="@color/ripple" /> diff --git a/app/src/main/res/layout/view_short_player.xml b/app/src/main/res/layout/view_short_player.xml index c067de8c..ca3f7c70 100644 --- a/app/src/main/res/layout/view_short_player.xml +++ b/app/src/main/res/layout/view_short_player.xml @@ -9,7 +9,7 @@ android:layout_above="@+id/short_player_progress_bar" android:background="@color/black" app:default_artwork="@drawable/placeholder_video_thumbnail" - app:resize_mode="fit" + app:resize_mode="fill" app:show_buffering="when_playing" app:use_artwork="true" app:use_controller="false" /> @@ -17,9 +17,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/shorts_button_attrs.xml b/app/src/main/res/values/shorts_button_attrs.xml new file mode 100644 index 00000000..de2738e0 --- /dev/null +++ b/app/src/main/res/values/shorts_button_attrs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/stable/assets/sources/apple-podcasts b/app/src/stable/assets/sources/apple-podcasts index 089987f0..8cff240c 160000 --- a/app/src/stable/assets/sources/apple-podcasts +++ b/app/src/stable/assets/sources/apple-podcasts @@ -1 +1 @@ -Subproject commit 089987f007319cf22972090a0cb09afd8c008adb +Subproject commit 8cff240ca7e9089ab26c03f78b6104d9cc2162fe diff --git a/app/src/stable/assets/sources/kick b/app/src/stable/assets/sources/kick index b7173f15..4ff0b027 160000 --- a/app/src/stable/assets/sources/kick +++ b/app/src/stable/assets/sources/kick @@ -1 +1 @@ -Subproject commit b7173f1538a8259ace0c606dfc3441426a659536 +Subproject commit 4ff0b02700fb5d52fe5bf4cf9bb379d21f6b6853 diff --git a/app/src/stable/assets/sources/peertube b/app/src/stable/assets/sources/peertube index 56bff391..21dcf4be 160000 --- a/app/src/stable/assets/sources/peertube +++ b/app/src/stable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit 56bff391239e676e7d347ad3730df17795938a7b +Subproject commit 21dcf4bef5847898752a7856b1208456a3031b6d diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble index 401274b1..3368dfaa 160000 --- a/app/src/stable/assets/sources/rumble +++ b/app/src/stable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 401274b1ec2806b3a61f877080ff023b4bf5dc0d +Subproject commit 3368dfaa2ccaaa060cbbc0e91c86200e4d927b6e diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify index 8c0f03f5..207738f5 160000 --- a/app/src/stable/assets/sources/spotify +++ b/app/src/stable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 8c0f03f5fbc9b4e499437b85c757ec40cb7c0126 +Subproject commit 207738f5997f52219c150d2122bd068c6aed2970 diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 2b724f21..95c60c2d 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2b724f21a727c3fefe16adb38f06aa8730b1b8ec +Subproject commit 95c60c2dc6aad556bb6fcc1e55795a412ac96340 diff --git a/app/src/unstable/assets/sources/apple-podcasts b/app/src/unstable/assets/sources/apple-podcasts index 089987f0..8cff240c 160000 --- a/app/src/unstable/assets/sources/apple-podcasts +++ b/app/src/unstable/assets/sources/apple-podcasts @@ -1 +1 @@ -Subproject commit 089987f007319cf22972090a0cb09afd8c008adb +Subproject commit 8cff240ca7e9089ab26c03f78b6104d9cc2162fe diff --git a/app/src/unstable/assets/sources/kick b/app/src/unstable/assets/sources/kick index b7173f15..4ff0b027 160000 --- a/app/src/unstable/assets/sources/kick +++ b/app/src/unstable/assets/sources/kick @@ -1 +1 @@ -Subproject commit b7173f1538a8259ace0c606dfc3441426a659536 +Subproject commit 4ff0b02700fb5d52fe5bf4cf9bb379d21f6b6853 diff --git a/app/src/unstable/assets/sources/peertube b/app/src/unstable/assets/sources/peertube index 56bff391..21dcf4be 160000 --- a/app/src/unstable/assets/sources/peertube +++ b/app/src/unstable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit 56bff391239e676e7d347ad3730df17795938a7b +Subproject commit 21dcf4bef5847898752a7856b1208456a3031b6d diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble index 401274b1..3368dfaa 160000 --- a/app/src/unstable/assets/sources/rumble +++ b/app/src/unstable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 401274b1ec2806b3a61f877080ff023b4bf5dc0d +Subproject commit 3368dfaa2ccaaa060cbbc0e91c86200e4d927b6e diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify index 8c0f03f5..207738f5 160000 --- a/app/src/unstable/assets/sources/spotify +++ b/app/src/unstable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 8c0f03f5fbc9b4e499437b85c757ec40cb7c0126 +Subproject commit 207738f5997f52219c150d2122bd068c6aed2970 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 2b724f21..95c60c2d 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2b724f21a727c3fefe16adb38f06aa8730b1b8ec +Subproject commit 95c60c2dc6aad556bb6fcc1e55795a412ac96340