From f63f9dd6db52f02d0efc44e1c952c3cebc3f14a5 Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 7 Mar 2025 14:27:18 -0600 Subject: [PATCH 01/25] initial POC shorts tab Changelog: added --- app/build.gradle | 11 + .../platformplayer/activities/MainActivity.kt | 8 +- .../bottombar/MenuBottomBarFragment.kt | 15 +- .../fragment/mainactivity/main/ShortView.kt | 427 ++++++++++++++++++ .../mainactivity/main/ShortsFragment.kt | 96 ++++ .../futo/platformplayer/states/StatePlayer.kt | 11 + .../views/video/FutoShortPlayer.kt | 179 ++++++++ .../views/video/FutoVideoPlayerBase.kt | 3 +- app/src/main/res/layout/activity_main.xml | 7 +- .../layout/fragment_overview_bottom_bar.xml | 2 +- app/src/main/res/layout/fragment_shorts.xml | 6 + app/src/main/res/layout/modal_comments.xml | 231 ++++++++++ app/src/main/res/layout/view_short.xml | 43 ++ app/src/main/res/layout/view_short_player.xml | 31 ++ app/src/main/res/values/colors.xml | 143 ++++++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/themes.xml | 36 ++ 17 files changed, 1235 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt create mode 100644 app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt create mode 100644 app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt create mode 100644 app/src/main/res/layout/fragment_shorts.xml create mode 100644 app/src/main/res/layout/modal_comments.xml create mode 100644 app/src/main/res/layout/view_short.xml create mode 100644 app/src/main/res/layout/view_short_player.xml diff --git a/app/build.gradle b/app/build.gradle index 8d55d000..5f38c749 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,6 +143,10 @@ android { } buildFeatures { buildConfig true + compose true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.2" } sourceSets { main { @@ -215,6 +219,7 @@ dependencies { //Database implementation("androidx.room:room-runtime:2.6.1") annotationProcessor("androidx.room:room-compiler:2.6.1") + debugImplementation 'androidx.compose.ui:ui-tooling:1.7.8' ksp("androidx.room:room-compiler:2.6.1") implementation("androidx.room:room-ktx:2.6.1") @@ -228,4 +233,10 @@ dependencies { testImplementation "org.mockito:mockito-core:5.4.0" androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + //Compose + def composeBom = platform('androidx.compose:compose-bom:2025.02.00') + implementation composeBom + androidTestImplementation composeBom + implementation 'androidx.compose.material3:material3' } diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 25febdb1..647cf6d8 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -1,13 +1,11 @@ package com.futo.platformplayer.activities import android.annotation.SuppressLint -import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager import android.content.res.Configuration -import android.media.AudioManager import android.net.Uri import android.os.Bundle import android.os.StrictMode @@ -57,6 +55,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsF import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment +import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment @@ -74,7 +73,6 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.UrlVideoWithTime -import com.futo.platformplayer.receivers.MediaButtonReceiver import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateBackup @@ -161,6 +159,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment; lateinit var _fragWatchlist: WatchLaterFragment; lateinit var _fragHistory: HistoryFragment; + lateinit var _fragShorts: ShortsFragment; lateinit var _fragSourceDetail: SourceDetailFragment; lateinit var _fragDownloads: DownloadsFragment; lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment; @@ -315,6 +314,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragPostDetail = PostDetailFragment.newInstance(); _fragWatchlist = WatchLaterFragment.newInstance(); _fragHistory = HistoryFragment.newInstance(); + _fragShorts = ShortsFragment.newInstance(); _fragSourceDetail = SourceDetailFragment.newInstance(); _fragDownloads = DownloadsFragment(); _fragImportSubscriptions = ImportSubscriptionsFragment.newInstance(); @@ -1088,6 +1088,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { if (segment.isMainView) { var transaction = supportFragmentManager.beginTransaction(); + transaction.setReorderingAllowed(true) if (segment.topBar != null) { if (segment.topBar != fragCurrent.topBar) { transaction = transaction @@ -1188,6 +1189,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { PostDetailFragment::class -> _fragPostDetail as T; WatchLaterFragment::class -> _fragWatchlist as T; HistoryFragment::class -> _fragHistory as T; + ShortsFragment::class -> _fragShorts as T; SourceDetailFragment::class -> _fragSourceDetail as T; DownloadsFragment::class -> _fragDownloads as T; ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index c4afcf74..2b7c055a 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -386,16 +386,17 @@ class MenuBottomBarFragment : MainActivityFragment() { it.navigate() } }), - ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate() }), - ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate() }), - ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate() }), - ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate() }), - ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate() }), - ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate() }), + ButtonDefinition(1, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.shorts, canToggle = true, { it.currentMain is ShortsFragment }, { it.navigate() }), + ButtonDefinition(2, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate() }), + ButtonDefinition(3, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate() }), + ButtonDefinition(4, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate() }), + ButtonDefinition(5, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate() }), + ButtonDefinition(6, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate() }), + ButtonDefinition(7, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate() }), ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate() }), ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate() }), ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate() }), - ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, { + ButtonDefinition(11, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, { val c = it.context ?: return@ButtonDefinition; Logger.i(TAG, "settings preventPictureInPicture()"); it.requireFragment().preventPictureInPicture(); 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 new file mode 100644 index 00000000..bba0dcee --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -0,0 +1,427 @@ +package com.futo.platformplayer.fragment.mainactivity.main + +import android.app.Dialog +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Animatable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.SoundEffectConstants +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ThumbUp +import androidx.compose.material.icons.outlined.ThumbUp +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RippleConfiguration +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.lifecycle.lifecycleScope +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.exceptions.ContentNotAvailableYetException +import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException +import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource +import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource +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.downloads.VideoLocal +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.logging.Logger +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.states.StatePlugins +import com.futo.platformplayer.views.video.FutoShortPlayer +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.OffsetDateTime +import kotlin.coroutines.cancellation.CancellationException + +@OptIn(ExperimentalMaterial3Api::class) +@UnstableApi +class ShortView : ConstraintLayout { + private var mainFragment: MainFragment? = null + private val player: FutoShortPlayer + private val overlayLoading: FrameLayout + private val overlayLoadingSpinner: ImageView + + private var url: String? = null + private var video: IPlatformVideo? = null + private var videoDetails: IPlatformVideoDetails? = null + + private var playWhenReady = false + + private var _lastVideoSource: IVideoSource? = null + private var _lastAudioSource: IAudioSource? = null + private var _lastSubtitleSource: ISubtitleSource? = null + + private var loadVideoJob: Job? = null + + private val bottomSheet: ModalBottomSheet = ModalBottomSheet() + + // Required constructor for XML inflation + constructor(context: Context) : super(context) { + inflate(context, R.layout.view_short, this) + + player = findViewById(R.id.short_player) + overlayLoading = findViewById(R.id.short_view_loading_overlay) + overlayLoadingSpinner = findViewById(R.id.short_view_loader) + + setupComposeView() + } + + // Required constructor for XML inflation with attributes + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + inflate(context, R.layout.view_short, this) + + player = findViewById(R.id.short_player) + overlayLoading = findViewById(R.id.short_view_loading_overlay) + overlayLoadingSpinner = findViewById(R.id.short_view_loader) + + setupComposeView() + } + + // Required constructor for XML inflation with attributes and style + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + inflate(context, R.layout.view_short, this) + + player = findViewById(R.id.short_player) + overlayLoading = findViewById(R.id.short_view_loading_overlay) + overlayLoadingSpinner = findViewById(R.id.short_view_loader) + + setupComposeView() + } + + constructor(inflater: LayoutInflater, fragment: MainFragment) : super(inflater.context) { + this.mainFragment = fragment + + inflater.inflate(R.layout.view_short, this, true) + player = findViewById(R.id.short_player) + overlayLoading = findViewById(R.id.short_view_loading_overlay) + overlayLoadingSpinner = findViewById(R.id.short_view_loader) + + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT + ) + } + + private fun setupComposeView () { + val composeView: ComposeView = findViewById(R.id.compose_view_test_button) + composeView.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + // In Compose world + MaterialTheme { + var checked by remember { mutableStateOf(false) } + + val tint = Color.White + + val alpha = 0.2f + val rippleConfiguration = + RippleConfiguration(color = tint, rippleAlpha = RippleAlpha(alpha, alpha, alpha, alpha)) + + val view = LocalView.current + + CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) { + IconToggleButton( + checked = checked, + onCheckedChange = { + checked = it + view.playSoundEffect(SoundEffectConstants.CLICK) + }, + ) { + if (checked) { + Icon( + Icons.Filled.ThumbUp, contentDescription = "Liked", tint = tint, + ) + } else { + Icon( + Icons.Outlined.ThumbUp, contentDescription = "Not Liked", tint = tint, + ) + } + } + } + } + } + } + } + + fun setMainFragment(fragment: MainFragment) { + this.mainFragment = fragment + } + + fun setVideo(url: String) { + if (url == this.url) { + return + } + + loadVideo(url) + } + + fun setVideo(video: IPlatformVideo) { + if (url == video.url) { + return + } + this.video = video + + loadVideo(video.url) + } + + fun setVideo(videoDetails: IPlatformVideoDetails) { + if (url == videoDetails.url) { + return + } + + this.videoDetails = videoDetails + } + + fun play() { + player.attach() + playVideo() + } + + fun stop() { + playWhenReady = false + + player.clear() + player.detach() + } + + fun detach() { + loadVideoJob?.cancel() + } + + private fun setLoading(isLoading: Boolean) { + if (isLoading) { + (overlayLoadingSpinner.drawable as Animatable?)?.start() + overlayLoading.visibility = View.VISIBLE + } else { + overlayLoading.visibility = View.GONE + (overlayLoadingSpinner.drawable as Animatable?)?.stop() + } + } + + private fun loadVideo(url: String) { + loadVideoJob?.cancel() + + loadVideoJob = CoroutineScope(Dispatchers.Main).launch { + setLoading(true) + _lastVideoSource = null + _lastAudioSource = null + _lastSubtitleSource = null + + val result = try { + withContext(StateApp.instance.scope.coroutineContext) { + StatePlatform.instance.getContentDetails(url).await() + } + } catch (e: CancellationException) { + return@launch + } catch (e: NoPlatformClientException) { + Logger.w(TAG, "exception", e) + + UIDialogs.showDialog( + context, R.drawable.ic_sources, "No source enabled to support this video\n(${url})", null, null, 0, UIDialogs.Action( + "Close", { }, UIDialogs.ActionStyle.PRIMARY + ) + ) + return@launch + } catch (e: ScriptLoginRequiredException) { + Logger.w(TAG, "exception", e) + UIDialogs.showDialog(context, R.drawable.ic_security, "Authentication", e.message, null, 0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Login", { + val id = e.config.let { if (it is SourcePluginConfig) it.id else null } + val didLogin = + if (id == null) false else StatePlugins.instance.loginPlugin(context, id) { + loadVideo(url) + } + if (!didLogin) UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login") + }, UIDialogs.ActionStyle.PRIMARY) + ) + return@launch + } catch (e: ContentNotAvailableYetException) { + Logger.w(TAG, "exception", e) + UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${e.availableWhen}.", "Close") { } + return@launch + } catch (e: ScriptImplementationException) { + Logger.w(TAG, "exception", e) + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), e, { loadVideo(url) }, null, mainFragment) + return@launch + } catch (e: ScriptAgeException) { + Logger.w(TAG, "exception", e) + UIDialogs.showDialog( + context, R.drawable.ic_lock, "Age restricted video", e.message, null, 0, UIDialogs.Action("Close", { }, UIDialogs.ActionStyle.PRIMARY) + ) + return@launch + } catch (e: ScriptUnavailableException) { + Logger.w(TAG, "exception", e) + if (video?.datetime == null || video?.datetime!! < OffsetDateTime.now() + .minusHours(1) + ) { + UIDialogs.showDialog( + context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), context.getString(R.string.this_video_is_unavailable), null, 0, UIDialogs.Action(context.getString(R.string.close), { }, UIDialogs.ActionStyle.PRIMARY) + ) + } + + video?.let { StatePlatform.instance.clearContentDetailCache(it.url) } + return@launch + } catch (e: ScriptException) { + Logger.w(TAG, "exception", e) + + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), e, { loadVideo(url) }, null, mainFragment) + return@launch + } catch (e: Throwable) { + Logger.w(ChannelFragment.TAG, "Failed to load video.", e) + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), e, { loadVideo(url) }, null, mainFragment) + return@launch + } + + if (result !is IPlatformVideoDetails) { + Logger.w( + TAG, "Wrong content type", IllegalStateException("Expected media content, found ${result.contentType}") + ) + return@launch + } + + // if it's been canceled then don't set the video details + if (!isActive) { + return@launch + } + + videoDetails = result + video = result + + setLoading(false) + + if (playWhenReady) playVideo() + } + } + + private fun playVideo(resumePositionMs: Long = 0) { + val video = videoDetails + + if (video === null) { + playWhenReady = true + return + } + + bottomSheet.show(mainFragment!!.childFragmentManager, ModalBottomSheet.TAG) + + try { + val videoSource = _lastVideoSource + ?: player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount()) + val audioSource = _lastAudioSource + ?: player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context)) + val subtitleSource = _lastSubtitleSource + ?: (if (video is VideoLocal) video.subtitlesSources.firstOrNull() else null) + Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") + + if (videoSource == null && audioSource == null) { + UIDialogs.showDialog( + context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), context.getString(R.string.this_video_is_unavailable), null, 0, UIDialogs.Action(context.getString(R.string.close), { }, UIDialogs.ActionStyle.PRIMARY) + ) + StatePlatform.instance.clearContentDetailCache(video.url) + return + } + + val thumbnail = video.thumbnails.getHQThumbnail() + if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap() + .load(thumbnail).into(object : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + player.setArtwork(BitmapDrawable(resources, resource)) + } + + override fun onLoadCleared(placeholder: Drawable?) { + player.setArtwork(null) + } + }) + else player.setArtwork(null) + player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0) + if (subtitleSource != null) player.swapSubtitles(mainFragment!!.lifecycleScope, subtitleSource) + player.seekTo(resumePositionMs) + + _lastVideoSource = videoSource + _lastAudioSource = audioSource + _lastSubtitleSource = subtitleSource + } catch (ex: UnsupportedCastException) { + Logger.e(TAG, "Failed to load cast media", ex) + UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.unsupported_cast_format), ex) + } catch (ex: Throwable) { + Logger.e(TAG, "Failed to load media", ex) + UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex) + } + } + + companion object { + const val TAG = "VideoDetailView" + } + + class ModalBottomSheet : BottomSheetDialogFragment() { + override fun onCreateDialog( + savedInstanceState: Bundle?, + ): Dialog { + val bottomSheetDialog = BottomSheetDialog( + requireContext() + ) + bottomSheetDialog.setContentView(R.layout.modal_comments) + + val composeView = bottomSheetDialog.findViewById(R.id.compose_view) + + composeView?.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + // In Compose world + MaterialTheme { + val view = LocalView.current + IconButton(onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + }) { + Icon( + Icons.Outlined.ThumbUp, contentDescription = "Close Bottom Sheet" + ) + } + } + } + } + return bottomSheetDialog + } + + companion object { + const val TAG = "ModalBottomSheet" + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..b11a30a4 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortsFragment.kt @@ -0,0 +1,96 @@ +package com.futo.platformplayer.fragment.mainactivity.main + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.OptIn +import androidx.core.view.get +import androidx.media3.common.util.UnstableApi +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import com.futo.platformplayer.R + +@UnstableApi +class ShortsFragment : MainFragment() { + override val isMainView: Boolean = true + override val isTab: Boolean = true + override val hasBottomBar: Boolean get() = true + + private var previousShownView: ShortView? = null + + private lateinit var viewPager: ViewPager2 + private lateinit var customViewAdapter: CustomViewAdapter + private val urls = listOf( + "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra", "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra" + ) + + override fun onCreateMainView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment_shorts, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewPager = view.findViewById(R.id.viewPager) + + customViewAdapter = CustomViewAdapter(urls, layoutInflater, this) + viewPager.adapter = customViewAdapter + + // TODO something is laggy sometimes when swiping between videos + viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + @OptIn(UnstableApi::class) + override fun onPageSelected(position: Int) { + previousShownView?.stop() + + val focusedView = + ((viewPager[0] as RecyclerView).findViewHolderForAdapterPosition(position) as CustomViewHolder).shortView + focusedView.play() + + + previousShownView = focusedView + } + + }) + + } + + override fun onPause() { + super.onPause() + previousShownView?.stop() + } + + companion object { + private const val TAG = "ShortsFragment" + + fun newInstance() = ShortsFragment() + } + + class CustomViewAdapter( + private val urls: List, private val inflater: LayoutInflater, private val fragment: MainFragment + ) : RecyclerView.Adapter() { + @OptIn(UnstableApi::class) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder { + val shortView = ShortView(inflater, fragment) + return CustomViewHolder(shortView) + } + + @OptIn(UnstableApi::class) + override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { + holder.shortView.setVideo(urls[position]) + } + + @OptIn(UnstableApi::class) + override fun onViewRecycled(holder: CustomViewHolder) { + super.onViewRecycled(holder) + holder.shortView.detach() + } + + override fun getItemCount(): Int = urls.size + } + + @OptIn(UnstableApi::class) + class CustomViewHolder(val shortView: ShortView) : RecyclerView.ViewHolder(shortView) +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt index b8368ea5..b12889e8 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt @@ -38,6 +38,7 @@ class StatePlayer { //Players private var _exoplayer : PlayerManager? = null; private var _thumbnailExoPlayer : PlayerManager? = null; + private var _shortExoPlayer: PlayerManager? = null //Video Status var rotationLock: Boolean = false @@ -633,6 +634,13 @@ class StatePlayer { } return _thumbnailExoPlayer!!; } + fun getShortPlayerOrCreate(context: Context) : PlayerManager { + if(_shortExoPlayer == null) { + val player = createExoPlayer(context); + _shortExoPlayer = PlayerManager(player); + } + return _shortExoPlayer!!; + } @OptIn(UnstableApi::class) private fun createExoPlayer(context : Context): ExoPlayer { @@ -656,10 +664,13 @@ class StatePlayer { fun dispose(){ val player = _exoplayer; val thumbPlayer = _thumbnailExoPlayer; + val shortPlayer = _shortExoPlayer _exoplayer = null; _thumbnailExoPlayer = null; + _shortExoPlayer = null player?.release(); thumbPlayer?.release(); + shortPlayer?.release() } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt new file mode 100644 index 00000000..9c2b003f --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt @@ -0,0 +1,179 @@ +package com.futo.platformplayer.views.video + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.animation.LinearInterpolator +import androidx.annotation.OptIn +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.DefaultTimeBar +import androidx.media3.ui.PlayerView +import androidx.media3.ui.TimeBar +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.api.media.models.video.IPlatformVideoDetails +import com.futo.platformplayer.helpers.VideoHelper +import com.futo.platformplayer.states.StatePlayer + +@UnstableApi +class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : + FutoVideoPlayerBase(PLAYER_STATE_NAME, context, attrs) { + + companion object { + private const val TAG = "FutoShortVideoPlayer" + private const val PLAYER_STATE_NAME: String = "ShortPlayer" + } + + private var playerAttached = false + + private val videoView: PlayerView + private val progressBar: DefaultTimeBar + + private val loadArtwork = object : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + setArtwork(BitmapDrawable(resources, resource)) + } + + override fun onLoadCleared(placeholder: Drawable?) { + setArtwork(null) + } + } + + private val player = StatePlayer.instance.getShortPlayerOrCreate(context) + + private var progressAnimator: ValueAnimator = createProgressBarAnimator() + + private var playerEventListener = object : Player.Listener { + override fun onEvents(player: Player, events: Player.Events) { + if (events.containsAny( + Player.EVENT_POSITION_DISCONTINUITY, Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED + ) + ) { + if (player.duration >= 0) { + progressAnimator.duration = player.duration + setProgressBarDuration(player.duration) + progressAnimator.currentPlayTime = player.currentPosition + } + + if (player.isPlaying) { + if (!progressAnimator.isStarted) { + progressAnimator.start() + } + } else { + if (progressAnimator.isRunning) { + progressAnimator.cancel() + } + } + } + } + } + + init { + LayoutInflater.from(context).inflate(R.layout.view_short_player, this, true) + videoView = findViewById(R.id.video_player) + progressBar = findViewById(R.id.video_player_progress_bar) + + progressBar.addListener(object : TimeBar.OnScrubListener { + override fun onScrubStart(timeBar: TimeBar, position: Long) { + if (progressAnimator.isRunning) { + progressAnimator.cancel() + } + } + + override fun onScrubMove(timeBar: TimeBar, position: Long) {} + + override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { + if (canceled) { + progressAnimator.currentPlayTime = player.player.currentPosition + progressAnimator.start() + return + } + + // the progress bar should never be available to the user without the player being attached to this view + assert(playerAttached) + seekTo(position) + } + }) + } + + @OptIn(UnstableApi::class) + private fun createProgressBarAnimator(): ValueAnimator { + return ValueAnimator.ofFloat(0f, 1f).apply { + interpolator = LinearInterpolator() + + addUpdateListener { animation -> + val progress = animation.animatedValue as Float + val duration = animation.duration + progressBar.setPosition((progress * duration).toLong()) + } + } + } + + fun setProgressBarDuration(duration: Long) { + progressBar.setDuration(duration) + } + + /** + * Attaches this short player instance to the exo player instance for shorts + */ + fun attach() { + // connect the exo player for shorts to the view for this instance + player.attach(videoView, PLAYER_STATE_NAME) + + // direct the base player what exo player instance to use + changePlayer(player) + + playerAttached = true + + player.player.addListener(playerEventListener) + } + + fun detach() { + playerAttached = false + player.player.removeListener(playerEventListener) + player.detach() + } + + fun setPreview(video: IPlatformVideoDetails) { + if (video.live != null) { + setSource(video.live, null, play = true, keepSubtitles = false) + } else { + val videoSource = + VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS) + val audioSource = + VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context)) + if (videoSource == null && audioSource != null) { + val thumbnail = video.thumbnails.getHQThumbnail() + if (!thumbnail.isNullOrBlank()) { + Glide.with(videoView).asBitmap().load(thumbnail).into(loadArtwork) + } else { + Glide.with(videoView).clear(loadArtwork) + setArtwork(null) + } + } else { + Glide.with(videoView).clear(loadArtwork) + } + + setSource(videoSource, audioSource, play = true, keepSubtitles = false) + } + } + + @OptIn(UnstableApi::class) + fun setArtwork(drawable: Drawable?) { + if (drawable != null) { + videoView.defaultArtwork = drawable + videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL + } else { + videoView.defaultArtwork = null + videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF + } + } +} diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index c872ca02..c3eddb12 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.util.AttributeSet import android.widget.RelativeLayout import androidx.annotation.OptIn +import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.coroutineScope import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.media3.common.C @@ -68,7 +69,7 @@ import java.io.ByteArrayInputStream import java.io.File import kotlin.math.abs -abstract class FutoVideoPlayerBase : RelativeLayout { +abstract class FutoVideoPlayerBase : ConstraintLayout { private val TAG = "FutoVideoPlayerBase" private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory(); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1708eeb4..df3abc69 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,9 +1,10 @@ - - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_overview_bottom_bar.xml b/app/src/main/res/layout/fragment_overview_bottom_bar.xml index 11c33b2c..30b6b752 100644 --- a/app/src/main/res/layout/fragment_overview_bottom_bar.xml +++ b/app/src/main/res/layout/fragment_overview_bottom_bar.xml @@ -38,7 +38,7 @@ + diff --git a/app/src/main/res/layout/modal_comments.xml b/app/src/main/res/layout/modal_comments.xml new file mode 100644 index 00000000..e079a671 --- /dev/null +++ b/app/src/main/res/layout/modal_comments.xml @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_short.xml b/app/src/main/res/layout/view_short.xml new file mode 100644 index 00000000..8ec7a845 --- /dev/null +++ b/app/src/main/res/layout/view_short.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_short_player.xml b/app/src/main/res/layout/view_short_player.xml new file mode 100644 index 00000000..bfd8762d --- /dev/null +++ b/app/src/main/res/layout/view_short_player.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6d142d08..603193fb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -38,4 +38,147 @@ #B3000000 #ACACAC #C25353 + + + #8F4C38 + #FFFFFF + #FFDBD1 + #723523 + #77574E + #FFFFFF + #FFDBD1 + #5D4037 + #6C5D2F + #FFFFFF + #F5E1A7 + #534619 + #BA1A1A + #FFFFFF + #FFDAD6 + #93000A + #FFF8F6 + #231917 + #FFF8F6 + #231917 + #F5DED8 + #53433F + #85736E + #D8C2BC + #000000 + #392E2B + #FFEDE8 + #FFB5A0 + #FFDBD1 + #3A0B01 + #FFB5A0 + #723523 + #FFDBD1 + #2C150F + #E7BDB2 + #5D4037 + #F5E1A7 + #231B00 + #D8C58D + #534619 + #E8D6D2 + #FFF8F6 + #FFFFFF + #FFF1ED + #FCEAE5 + #F7E4E0 + #F1DFDA + #5D2514 + #FFFFFF + #A15A45 + #FFFFFF + #4B2F28 + #FFFFFF + #87655C + #FFFFFF + #41350A + #FFFFFF + #7B6C3C + #FFFFFF + #740006 + #FFFFFF + #CF2C27 + #FFFFFF + #FFF8F6 + #231917 + #FFF8F6 + #180F0D + #F5DED8 + #41332F + #5F4F4A + #7B6964 + #000000 + #392E2B + #FFEDE8 + #FFB5A0 + #A15A45 + #FFFFFF + #84422F + #FFFFFF + #87655C + #FFFFFF + #6D4D45 + #FFFFFF + #7B6C3C + #FFFFFF + #615426 + #FFFFFF + #D4C3BE + #FFF8F6 + #FFFFFF + #FFF1ED + #F7E4E0 + #EBD9D4 + #DFCEC9 + #501B0B + #FFFFFF + #753725 + #FFFFFF + #3F261E + #FFFFFF + #60423A + #FFFFFF + #362B02 + #FFFFFF + #55481C + #FFFFFF + #600004 + #FFFFFF + #98000A + #FFFFFF + #FFF8F6 + #231917 + #FFF8F6 + #000000 + #F5DED8 + #000000 + #372925 + #554641 + #000000 + #392E2B + #FFFFFF + #FFB5A0 + #753725 + #FFFFFF + #592111 + #FFFFFF + #60423A + #FFFFFF + #472C24 + #FFFFFF + #55481C + #FFFFFF + #3D3206 + #FFFFFF + #C6B5B1 + #FFF8F6 + #FFFFFF + #FFEDE8 + #F1DFDA + #E2D1CC + #D4C3BE \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4df1905..2285becf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ Failed to retrieve data, are you connected? Settings History + Shorts Sources Buy FAQ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 8fa8f2d1..3f2b80c5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -49,6 +49,42 @@ @style/Theme.FutoVideo.ListView @style/Theme.FutoVideo.TextView @style/Theme.FutoVideo.CheckedTextView + + + @color/md_theme_inversePrimary + @color/md_theme_primaryContainer + @color/md_theme_onPrimaryContainer + @color/md_theme_secondaryContainer + @color/md_theme_onSecondaryContainer + @color/md_theme_tertiary + @color/md_theme_onTertiary + @color/md_theme_tertiaryContainer + @color/md_theme_onTertiaryContainer + @color/md_theme_surfaceVariant + @color/md_theme_onSurfaceVariant + @color/md_theme_inverseSurface + @color/md_theme_inverseOnSurface + @color/md_theme_outline + @color/md_theme_errorContainer + @color/md_theme_onErrorContainer + @style/TextAppearance.Material3.DisplayLarge + @style/TextAppearance.Material3.DisplayMedium + @style/TextAppearance.Material3.DisplaySmall + @style/TextAppearance.Material3.HeadlineLarge + @style/TextAppearance.Material3.HeadlineMedium + @style/TextAppearance.Material3.HeadlineSmall + @style/TextAppearance.Material3.TitleLarge + @style/TextAppearance.Material3.TitleMedium + @style/TextAppearance.Material3.TitleSmall + @style/TextAppearance.Material3.BodyLarge + @style/TextAppearance.Material3.BodyMedium + @style/TextAppearance.Material3.BodySmall + @style/TextAppearance.Material3.LabelLarge + @style/TextAppearance.Material3.LabelMedium + @style/TextAppearance.Material3.LabelSmall + @style/ShapeAppearance.Material3.SmallComponent + @style/ShapeAppearance.Material3.MediumComponent + @style/ShapeAppearance.Material3.LargeComponent + + + + + \ No newline at end of file From a1c2d19daf1a6e859598421e27464e4e98388d4f Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 8 Apr 2025 17:58:31 -0500 Subject: [PATCH 04/25] refactor shorts code Changelog: changed --- .../fragment/mainactivity/main/ShortView.kt | 599 +++++++---------- .../mainactivity/main/ShortsFragment.kt | 32 +- .../views/overlays/WebviewOverlay.kt | 4 +- .../views/video/FutoShortPlayer.kt | 72 +- .../views/video/FutoVideoPlayerBase.kt | 2 +- app/src/main/res/layout/fragment_shorts.xml | 4 +- app/src/main/res/layout/modal_comments.xml | 619 ++++++++---------- app/src/main/res/layout/view_short.xml | 6 +- app/src/main/res/layout/view_short_player.xml | 6 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 2 +- 11 files changed, 545 insertions(+), 802 deletions(-) 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 2a9f954f..1bef3548 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 @@ -12,11 +12,9 @@ import android.os.Bundle import android.text.Spanned import android.util.AttributeSet import android.util.TypedValue -import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.SoundEffectConstants import android.view.View -import android.view.ViewGroup import android.widget.Button import android.widget.FrameLayout import android.widget.ImageView @@ -29,16 +27,13 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -51,7 +46,6 @@ import androidx.compose.material.icons.filled.ThumbDownOffAlt import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.filled.ThumbUpOffAlt import androidx.compose.material.icons.outlined.Share -import androidx.compose.material.icons.outlined.ThumbUp import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.ripple.RippleAlpha @@ -74,7 +68,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.platform.ComposeView @@ -101,6 +94,7 @@ 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 @@ -120,11 +114,10 @@ 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 @@ -173,7 +166,6 @@ 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.theme.overlay.MaterialThemeOverlay import com.google.protobuf.ByteString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -184,7 +176,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import userpackage.Protocol import java.time.OffsetDateTime -import kotlin.contracts.Effect import kotlin.coroutines.cancellation.CancellationException @OptIn(ExperimentalMaterial3Api::class) @@ -207,8 +198,6 @@ class ShortView : ConstraintLayout { private var playWhenReady = false -// private var playerAttached = false - private var _lastVideoSource: IVideoSource? = null private var _lastAudioSource: IAudioSource? = null private var _lastSubtitleSource: ISubtitleSource? = null @@ -285,54 +274,55 @@ class ShortView : ConstraintLayout { setupComposeView() } + // TODO merge this with the updateQualitySourcesOverlay for the normal video player @androidx.annotation.OptIn(UnstableApi::class) private fun updateQualitySourcesOverlay(videoDetails: IPlatformVideoDetails?, videoLocal: VideoLocal? = null, liveStreamVideoFormats: List? = null, liveStreamAudioFormats: List? = null) { - Logger.i(TAG, "updateQualitySourcesOverlay"); + Logger.i(TAG, "updateQualitySourcesOverlay") - val video: IPlatformVideoDetails?; - val localVideoSources: List?; - val localAudioSource: List?; - val localSubtitleSources: List?; + val video: IPlatformVideoDetails? + val localVideoSources: List? + val localAudioSource: List? + val localSubtitleSources: List? - val videoSources: List?; - val audioSources: List?; + val videoSources: List? + val audioSources: List? if (videoDetails is VideoLocal) { - video = videoLocal?.videoSerialized; - localVideoSources = videoDetails.videoSource.toList(); - localAudioSource = videoDetails.audioSource.toList(); - localSubtitleSources = videoDetails.subtitlesSources.toList(); + video = videoLocal?.videoSerialized + localVideoSources = videoDetails.videoSource.toList() + localAudioSource = videoDetails.audioSource.toList() + localSubtitleSources = videoDetails.subtitlesSources.toList() videoSources = null - audioSources = null; + audioSources = null } else { - video = videoDetails; - videoSources = video?.video?.videoSources?.toList(); + video = videoDetails + videoSources = video?.video?.videoSources?.toList() audioSources = if (video?.video?.isUnMuxed == true) (video.video as VideoUnMuxedSourceDescriptor).audioSources.toList() else null if (videoLocal != null) { - localVideoSources = videoLocal.videoSource.toList(); - localAudioSource = videoLocal.audioSource.toList(); - localSubtitleSources = videoLocal.subtitlesSources.toList(); + localVideoSources = videoLocal.videoSource.toList() + localAudioSource = videoLocal.audioSource.toList() + localSubtitleSources = videoLocal.subtitlesSources.toList() } else { - localVideoSources = null; - localAudioSource = null; - localSubtitleSources = null; + localVideoSources = null + localAudioSource = null + localSubtitleSources = null } } - val doDedup = Settings.instance.playback.simplifySources; + val doDedup = Settings.instance.playback.simplifySources val bestVideoSources = if (doDedup) (videoSources?.map { it.height * it.width }?.distinct() ?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) } ?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource }))?.distinct() ?.filterNotNull()?.toList() ?: listOf() else videoSources?.toList() ?: listOf() val bestAudioContainer = - audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container }; + audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container } val bestAudioSources = if (doDedup) audioSources?.filter { it.container == bestAudioContainer } ?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource }) - ?.distinct()?.toList() ?: listOf() else audioSources?.toList() ?: listOf(); + ?.distinct()?.toList() ?: listOf() else audioSources?.toList() ?: listOf() val canSetSpeed = true val currentPlaybackRate = player.getPlaybackRate() @@ -340,117 +330,94 @@ class ShortView : ConstraintLayout { SlideUpMenuOverlay(this.context, overlayQualityContainer, context.getString( R.string.quality ), null, true, if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null, if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply { - setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate!!.toString()); + setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate.toString()) onClick.subscribe { v -> - player.setPlaybackRate(v.toFloat()); - setSelected(v); + player.setPlaybackRate(v.toFloat()) + setSelected(v) - }; - } else null, - - if (localVideoSources?.isNotEmpty() == true) SlideUpMenuGroup( - this.context, context.getString(R.string.offline_video), "video", *localVideoSources.map { - SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", tag = it, call = { handleSelectVideoTrack(it) }); - }.toList().toTypedArray() - ) - else null, if (localAudioSource?.isNotEmpty() == true) SlideUpMenuGroup( - this.context, context.getString(R.string.offline_audio), "audio", *localAudioSource.map { - SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), tag = it, call = { handleSelectAudioTrack(it) }); - }.toList().toTypedArray() - ) - else null, if (localSubtitleSources?.isNotEmpty() == true) SlideUpMenuGroup( - this.context, context.getString(R.string.offline_subtitles), "subtitles", *localSubtitleSources.map { - SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) }) - }.toList().toTypedArray() - ) - else null, if (liveStreamVideoFormats?.isEmpty() == false) SlideUpMenuGroup( - this.context, context.getString(R.string.stream_video), "video", (listOf( - SlideUpMenuItem(this.context, R.drawable.ic_movie, "Auto", tag = "auto", call = { player.selectVideoTrack(-1) }) - ) + (liveStreamVideoFormats.map { - SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label - ?: it.containerMimeType - ?: it.bitrate.toString(), "${it.width}x${it.height}", tag = it, call = { player.selectVideoTrack(it.height) }); - })) - ) - else null, if (liveStreamAudioFormats?.isEmpty() == false) SlideUpMenuGroup( - this.context, context.getString(R.string.stream_audio), "audio", *liveStreamAudioFormats.map { - SlideUpMenuItem(this.context, R.drawable.ic_music, "${it.label ?: it.containerMimeType} ${it.bitrate}", "", tag = it, call = { player.selectAudioTrack(it.bitrate) }); - }.toList().toTypedArray() - ) - else null, - - if (bestVideoSources.isNotEmpty()) SlideUpMenuGroup( - this.context, context.getString(R.string.video), "video", *bestVideoSources.map { - val estSize = VideoHelper.estimateSourceSize(it); - val prefix = - if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; - SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectVideoTrack(it) }); - }.toList().toTypedArray() - ) - else null, if (bestAudioSources.isNotEmpty()) SlideUpMenuGroup( - this.context, context.getString(R.string.audio), "audio", *bestAudioSources.map { - val estSize = VideoHelper.estimateSourceSize(it); - val prefix = - if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; - SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) }); - }.toList().toTypedArray() - ) - else null, if (video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup( - this.context, context.getString(R.string.subtitles), "subtitles", *video.subtitles.map { - SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) }) - }.toList().toTypedArray() - ) - else null - ); + } + } else null, if (localVideoSources?.isNotEmpty() == true) SlideUpMenuGroup( + this.context, context.getString(R.string.offline_video), "video", *localVideoSources.map { + SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", tag = it, call = { handleSelectVideoTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (localAudioSource?.isNotEmpty() == true) SlideUpMenuGroup( + this.context, context.getString(R.string.offline_audio), "audio", *localAudioSource.map { + SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), tag = it, call = { handleSelectAudioTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (localSubtitleSources?.isNotEmpty() == true) SlideUpMenuGroup( + this.context, context.getString(R.string.offline_subtitles), "subtitles", *localSubtitleSources.map { + SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (liveStreamVideoFormats?.isEmpty() == false) SlideUpMenuGroup( + this.context, context.getString(R.string.stream_video), "video", (listOf( + SlideUpMenuItem(this.context, R.drawable.ic_movie, "Auto", tag = "auto", call = { player.selectVideoTrack(-1) }) + ) + (liveStreamVideoFormats.map { + SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label + ?: it.containerMimeType + ?: it.bitrate.toString(), "${it.width}x${it.height}", tag = it, call = { player.selectVideoTrack(it.height) }) + })) + ) + else null, if (liveStreamAudioFormats?.isEmpty() == false) SlideUpMenuGroup( + this.context, context.getString(R.string.stream_audio), "audio", *liveStreamAudioFormats.map { + SlideUpMenuItem(this.context, R.drawable.ic_music, "${it.label ?: it.containerMimeType} ${it.bitrate}", "", tag = it, call = { player.selectAudioTrack(it.bitrate) }) + }.toList().toTypedArray() + ) + else null, if (bestVideoSources.isNotEmpty()) SlideUpMenuGroup( + this.context, context.getString(R.string.video), "video", *bestVideoSources.map { + val estSize = VideoHelper.estimateSourceSize(it) + val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "" + SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectVideoTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (bestAudioSources.isNotEmpty()) SlideUpMenuGroup( + this.context, context.getString(R.string.audio), "audio", *bestAudioSources.map { + val estSize = VideoHelper.estimateSourceSize(it) + val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "" + SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) }) + }.toList().toTypedArray() + ) + else null, if (video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup( + this.context, context.getString(R.string.subtitles), "subtitles", *video.subtitles.map { + SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", tag = it, call = { handleSelectSubtitleTrack(it) }) + }.toList().toTypedArray() + ) + else null + ) } private fun handleSelectVideoTrack(videoSource: IVideoSource) { Logger.i(TAG, "handleSelectAudioTrack(videoSource=$videoSource)") - val video = videoDetails ?: return; + if (_lastVideoSource == videoSource) return - if (_lastVideoSource == videoSource) return; - - val d = StateCasting.instance.activeDevice; - if (d != null && d.connectionState == CastConnectionState.CONNECTED) StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); - else if (!player.swapSources(videoSource, _lastAudioSource, true, true, true)) player.hideControls(false); //TODO: Disable player? - - _lastVideoSource = videoSource; + _lastVideoSource = videoSource } private fun handleSelectAudioTrack(audioSource: IAudioSource) { Logger.i(TAG, "handleSelectAudioTrack(audioSource=$audioSource)") - val video = videoDetails ?: return; + if (_lastAudioSource == audioSource) return - if (_lastAudioSource == audioSource) return; - - val d = StateCasting.instance.activeDevice; - if (d != null && d.connectionState == CastConnectionState.CONNECTED) StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); - else (!player.swapSources(_lastVideoSource, audioSource, true, true, true)) - player.hideControls(false); //TODO: Disable player? - - _lastAudioSource = audioSource; + _lastAudioSource = audioSource } private fun handleSelectSubtitleTrack(subtitleSource: ISubtitleSource) { Logger.i(TAG, "handleSelectSubtitleTrack(subtitleSource=$subtitleSource)") - val video = videoDetails ?: return; - var toSet: ISubtitleSource? = subtitleSource - if (_lastSubtitleSource == subtitleSource) toSet = null; + if (_lastSubtitleSource == subtitleSource) toSet = null - val d = StateCasting.instance.activeDevice; - if (d != null && d.connectionState == CastConnectionState.CONNECTED) StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); - else player.swapSubtitles(mainFragment!!.lifecycleScope, toSet); + player.swapSubtitles(mainFragment.lifecycleScope, toSet) - _lastSubtitleSource = toSet; + _lastSubtitleSource = toSet } private fun showVideoSettings() { Logger.i(TAG, "showVideoSettings") - overlayQualitySelector?.selectOption("video", _lastVideoSource); - overlayQualitySelector?.selectOption("audio", _lastAudioSource); - overlayQualitySelector?.selectOption("subtitles", _lastSubtitleSource); + overlayQualitySelector?.selectOption("video", _lastVideoSource) + overlayQualitySelector?.selectOption("audio", _lastAudioSource) + overlayQualitySelector?.selectOption("subtitles", _lastSubtitleSource) if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource) { @@ -484,19 +451,18 @@ class ShortView : ConstraintLayout { } } - val currentPlaybackRate = player.getPlaybackRate() ?: 1.0 + val currentPlaybackRate = player.getPlaybackRate() overlayQualitySelector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" } ?.let { (it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString()) - }; + } - overlayQualitySelector?.show(); -// _slideUpOverlay = overlayQualitySelector; + overlayQualitySelector?.show() } @OptIn(ExperimentalGlideComposeApi::class) private fun setupComposeView() { - val composeView: ComposeView = findViewById(R.id.compose_view_test_button) + val composeView: ComposeView = findViewById(R.id.shorts_overlay_content_compose_view) composeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -554,8 +520,7 @@ class ShortView : ConstraintLayout { val tint = Color.White val buttonTextStyle = TextStyle( - fontSize = 12.sp, - shadow = Shadow( + fontSize = 12.sp, shadow = Shadow( color = Color.Black, blurRadius = 3f ) ) @@ -571,27 +536,21 @@ class ShortView : ConstraintLayout { ConstraintLayout(modifier = Modifier.fillMaxSize()) { val (title, buttons) = createRefs() -// val horizontalChain = createHorizontalChain(title, buttons, chainStyle = ChainStyle.SpreadInside) Box(modifier = Modifier.constrainAs(title) { -// top.linkTo(parent.top) bottom.linkTo(parent.bottom, margin = 16.dp) start.linkTo(parent.start, margin = 8.dp) end.linkTo(buttons.start) width = Dimension.fillToConstraints - } -// .fillMaxWidth() - ) { + }) { Column( - modifier = Modifier - .align(Alignment.BottomStart), - verticalArrangement = Arrangement.spacedBy(4.dp) + modifier = Modifier.align(Alignment.BottomStart), verticalArrangement = Arrangement.spacedBy(4.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.clickable(onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) - mainFragment!!.navigate(currentVideo?.author) + mainFragment.navigate(currentVideo?.author) }), ) { @@ -601,27 +560,23 @@ class ShortView : ConstraintLayout { .clip(CircleShape) ) Text( - currentVideo?.author?.name ?: "", - color = tint, - fontSize = 14.sp + currentVideo?.author?.name + ?: "", color = tint, fontSize = 14.sp ) } Text( currentVideo?.name - ?: "", color = tint, maxLines = 1, overflow = TextOverflow.Ellipsis, - fontSize = 14.sp + ?: "", color = tint, maxLines = 1, overflow = TextOverflow.Ellipsis, fontSize = 14.sp ) } } Box(modifier = Modifier.constrainAs(buttons) { -// top.linkTo(parent.top) bottom.linkTo(parent.bottom, margin = 16.dp) start.linkTo(title.end, margin = 12.dp) end.linkTo(parent.end, margin = 4.dp) marginBottom - }) { CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) { Column( @@ -667,10 +622,7 @@ class ShortView : ConstraintLayout { } Text( likes.toString(), color = tint, - modifier = Modifier - .align(Alignment.BottomCenter) -// .offset(y = buttonOffset) - , + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } @@ -713,30 +665,30 @@ class ShortView : ConstraintLayout { } Text( dislikes.toString(), color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } } Box { IconButton( - modifier = Modifier.padding(bottom = buttonOffset).align(Alignment.TopCenter), + modifier = Modifier + .padding(bottom = buttonOffset) + .align(Alignment.TopCenter), onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) - bottomSheet.show(mainFragment!!.childFragmentManager, CommentsModalBottomSheet.TAG) + if (!bottomSheet.isAdded) { + bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG) + } }, ) { Icon( Icons.AutoMirrored.Outlined.Comment, contentDescription = "View Comments", tint = tint, - ) + ) } Text( - "Comments", color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), - style = buttonTextStyle + "Comments", color = tint, modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle ) } Box { @@ -744,12 +696,13 @@ class ShortView : ConstraintLayout { modifier = Modifier.padding(bottom = buttonOffset), onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) - val url = currentVideo?.shareUrl ?: currentVideo?.url - mainFragment!!.startActivity(Intent.createChooser(Intent().apply { - action = Intent.ACTION_SEND; + val url = + currentVideo?.shareUrl ?: currentVideo?.url + mainFragment.startActivity(Intent.createChooser(Intent().apply { + action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, url) type = "text/plain" - }, null)); + }, null)) }, ) { Icon( @@ -758,8 +711,7 @@ class ShortView : ConstraintLayout { } Text( "Share", color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } @@ -778,8 +730,7 @@ class ShortView : ConstraintLayout { } Text( "Refresh", color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } @@ -797,8 +748,7 @@ class ShortView : ConstraintLayout { } Text( "Quality", color = tint, - modifier = Modifier - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), style = buttonTextStyle, ) } @@ -822,9 +772,6 @@ class ShortView : ConstraintLayout { modifier = Modifier .size(94.dp) .background(color = Color.Black.copy(alpha = 0.7f), shape = CircleShape), contentAlignment = Alignment.Center -// .padding(32.dp), // Pad the entire box - - ) { Icon( imageVector = if (isPlaying) Icons.Rounded.PlayArrow @@ -836,7 +783,7 @@ class ShortView : ConstraintLayout { // Auto-hide the icon after a short delay LaunchedEffect(showPlayPauseIcon) { if (showPlayPauseIcon) { - delay(1500) // Icon visible for 1.5 seconds + delay(1500) showPlayPauseIcon = false } } @@ -849,6 +796,7 @@ class ShortView : ConstraintLayout { } } + @Suppress("unused") fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) { this.mainFragment = fragment this.bottomSheet.mainFragment = fragment @@ -864,6 +812,7 @@ class ShortView : ConstraintLayout { loadVideo(video.url) } + @Suppress("unused") fun changeVideo(videoDetails: IPlatformVideoDetails) { if (video?.url == videoDetails.url) { return @@ -874,31 +823,16 @@ class ShortView : ConstraintLayout { } fun play() { -// if (playerAttached){ -// throw Exception() -// } -// playerAttached = true loadLikes(this.video!!) player.attach() -// if (_lastVideoSource != null || _lastAudioSource != null) { -// if (!player.activelyPlaying) { -// player.play() -// } -// } else { playVideo() -// } } - // fun pause() { player.pause() } fun stop() { -// if (!playerAttached) { -// return -// } -// playerAttached = false playWhenReady = false player.clear() @@ -928,7 +862,7 @@ class ShortView : ConstraintLayout { val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null } - mainFragment!!.lifecycleScope.launch(Dispatchers.IO) { + mainFragment.lifecycleScope.launch(Dispatchers.IO) { try { val queryReferencesResponse = ApiMethods.getQueryReferences( ApiMethods.SERVER, ref, null, null, arrayListOf( @@ -941,31 +875,29 @@ class ShortView : ConstraintLayout { ByteString.copyFrom(Opinion.dislike.data) ).build() ), extraByteReferences = listOfNotNull(extraBytesRef) - ); + ) - val likes = queryReferencesResponse.countsList[0]; - val dislikes = queryReferencesResponse.countsList[1]; - val hasLiked = - StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/; - val hasDisliked = - StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/; + val likes = queryReferencesResponse.countsList[0] + val dislikes = queryReferencesResponse.countsList[1] + val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray()) + val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray()) withContext(Dispatchers.Main) { onLikesLoaded.emit(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked) onLikeDislikeUpdated.subscribe(this) { args -> if (args.hasLiked) { - args.processHandle.opinion(ref, Opinion.like); + args.processHandle.opinion(ref, Opinion.like) } else if (args.hasDisliked) { - args.processHandle.opinion(ref, Opinion.dislike); + args.processHandle.opinion(ref, Opinion.dislike) } else { - args.processHandle.opinion(ref, Opinion.neutral); + args.processHandle.opinion(ref, Opinion.neutral) } - mainFragment!!.lifecycleScope.launch(Dispatchers.IO) { + mainFragment.lifecycleScope.launch(Dispatchers.IO) { try { - Logger.i(CommentsModalBottomSheet.Companion.TAG, "Started backfill"); - args.processHandle.fullyBackfillServersAnnounceExceptions(); - Logger.i(CommentsModalBottomSheet.Companion.TAG, "Finished backfill"); + Logger.i(CommentsModalBottomSheet.Companion.TAG, "Started backfill") + args.processHandle.fullyBackfillServersAnnounceExceptions() + Logger.i(CommentsModalBottomSheet.Companion.TAG, "Finished backfill") } catch (e: Throwable) { Logger.e(CommentsModalBottomSheet.Companion.TAG, "Failed to backfill servers", e) } @@ -974,10 +906,10 @@ class ShortView : ConstraintLayout { StatePolycentric.instance.updateLikeMap( ref, args.hasLiked, args.hasDisliked ) - }; + } } } catch (e: Throwable) { - Logger.e(CommentsModalBottomSheet.Companion.TAG, "Failed to get polycentric likes/dislikes.", e); + Logger.e(CommentsModalBottomSheet.Companion.TAG, "Failed to get polycentric likes/dislikes.", e) } } } @@ -1120,7 +1052,7 @@ class ShortView : ConstraintLayout { }) else player.setArtwork(null) player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0) - if (subtitleSource != null) player.swapSubtitles(mainFragment!!.lifecycleScope, subtitleSource) + if (subtitleSource != null) player.swapSubtitles(mainFragment.lifecycleScope, subtitleSource) player.seekTo(resumePositionMs) _lastVideoSource = videoSource @@ -1149,19 +1081,18 @@ class ShortView : ConstraintLayout { private lateinit var containerContentSupport: SupportOverlay private lateinit var title: TextView - private lateinit var subTitle: 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 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 buttonSubscribe: SubscribeButton + 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 @@ -1183,49 +1114,22 @@ class ShortView : ConstraintLayout { private lateinit var behavior: BottomSheetBehavior -// override fun getTheme(): Int { -// return R.style.CustomBottomSheetDialog -// } - -// override fun getTheme(): Int = R.style.CustomBottomSheetTheme - -// override fun onCreateView( -// inflater: LayoutInflater, -// container: ViewGroup?, -// savedInstanceState: Bundle? -// ): View? { -// -//// container. -// -// val bottomSheetDialog = inflater.inflate(R.layout.modal_comments, container, false) + private val _taskLoadPolycentricProfile = + TaskHandler(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }).success { it -> setPolycentricProfile(it, animate = true) } + .exception { + Logger.w(TAG, "Failed to load claims.", it) + } override fun onCreateDialog( savedInstanceState: Bundle?, ): Dialog { - val bottomSheetDialog = BottomSheetDialog( -// mainFragment!!.context!!, -// MaterialThemeOverlay.wrap(requireContext()) -// ContextThemeWrapper(requireContext(), ), -// ContextThemeWrapper( requireContext(), R.style.ThemeOverlay_App_Material3_BottomSheetDialog) - requireContext(),R.style.Custom_BottomSheetDialog_Theme -// MaterialThemeOverlay.wrap(requireContext()) -// ContextThemeWrapper(requireContext(), R.style.BottomSheetDialog_Rounded) -// R.style.ThemeOverlay_App_Material3_BottomSheetDialog -// BottomSheet -// R.style.CustomBottomSheetTheme -// R.style.ThemeOverlay_Cata -// R.style.ThemeOverlay_Catalog_BottomSheetDialog_Scrollable - //com.google.android.material.R.style.Animation_Design_BottomSheetDialog - ) + val bottomSheetDialog = + BottomSheetDialog(requireContext(), R.style.Custom_BottomSheetDialog_Theme) bottomSheetDialog.setContentView(R.layout.modal_comments) -// bottomSheetDialog.behavior. - behavior = bottomSheetDialog.behavior -// val composeView = bottomSheetDialog.findViewById(R.id.compose_view) - - containerContent = bottomSheetDialog.findViewById(R.id.contentContainer)!! + containerContent = bottomSheetDialog.findViewById(R.id.content_container)!! containerContentMain = bottomSheetDialog.findViewById(R.id.videodetail_container_main)!! containerContentReplies = bottomSheetDialog.findViewById(R.id.videodetail_container_replies)!! @@ -1248,8 +1152,6 @@ class ShortView : ConstraintLayout { imageLikeIcon = bottomSheetDialog.findViewById(R.id.image_like_icon)!! imageDislikeIcon = bottomSheetDialog.findViewById(R.id.image_dislike_icon)!! -// buttonSubscribe = bottomSheetDialog.findViewById(R.id.button_subscribe)!! - description = bottomSheetDialog.findViewById(R.id.videodetail_description)!! descriptionContainer = bottomSheetDialog.findViewById(R.id.videodetail_description_container)!! @@ -1263,45 +1165,42 @@ class ShortView : ConstraintLayout { commentsList.onAuthorClick.subscribe { c -> if (c !is PolycentricPlatformComment) { - return@subscribe; + return@subscribe } + val id = c.author.id.value - Logger.i(TAG, "onAuthorClick: " + c.author.id.value); - if (c.author.id.value?.startsWith("polycentric://") ?: false) { - val navUrl = - "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length); - //val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length); + Logger.i(TAG, "onAuthorClick: $id") + if (id != null && id.startsWith("polycentric://") == true) { + val navUrl = "https://harbor.social/" + id.substring("polycentric://".length) mainFragment!!.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl))) - //_container_content_browser.goto(navUrl); - //switchContentView(_container_content_browser); } } commentsList.onRepliesClick.subscribe { c -> - val replyCount = c.replyCount ?: 0; - var metadata = ""; + val replyCount = c.replyCount ?: 0 + var metadata = "" if (replyCount > 0) { - metadata += "$replyCount " + requireContext().getString(R.string.replies); + metadata += "$replyCount " + requireContext().getString(R.string.replies) } if (c is PolycentricPlatformComment) { - var parentComment: PolycentricPlatformComment = c; + 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; - }); + ) + commentsList.replaceComment(parentComment, newComment) + parentComment = newComment + }) } else { - containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) }); + containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) }) } - animateOpenOverlayView(containerContentReplies); + animateOpenOverlayView(containerContentReplies) } if (StatePolycentric.instance.enabled) { buttonPolycentric.setOnClickListener { - setTabIndex(0); - StateMeta.instance.setLastCommentSection(0); + setTabIndex(0) + StateMeta.instance.setLastCommentSection(0) } } else { buttonPolycentric.visibility = GONE @@ -1309,7 +1208,7 @@ class ShortView : ConstraintLayout { buttonPlatform.setOnClickListener { setTabIndex(1) - StateMeta.instance.setLastCommentSection(1); + StateMeta.instance.setLastCommentSection(1) } val ref = Models.referenceFromBuffer(video.url.toByteArray()) @@ -1325,10 +1224,6 @@ class ShortView : ConstraintLayout { } } -// val layoutTop: LinearLayout = bottomSheetDialog.findViewById(R.id.layout_top)!! -// containerContentMain.removeView(layoutTop) -// commentsList.setPrependedView(layoutTop) - containerContentDescription.onClose.subscribe { animateCloseOverlayView() } containerContentReplies.onClose.subscribe { animateCloseOverlayView() } @@ -1344,89 +1239,89 @@ class ShortView : ConstraintLayout { TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics) //UI - title.text = video.name; - channelName.text = video.author.name; + 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 ""; + ) 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); + 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); + 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)}"); + 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 diff = video.datetime?.getNowDiffSeconds() ?: 0 val ago = video.datetime?.toHumanNowDiffString(true) - if (diff >= 0) subTitleSegments.add("${ago} ago"); - else subTitleSegments.add("available in ${ago}"); + 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); + platform.setPlatformFromClientID(video.id.pluginId) + subTitle.text = subTitleSegments.joinToString(" • ") + creatorThumbnail.setThumbnail(video.author.thumbnail, false) - setPolycentricProfile(null, animate = false); + setPolycentricProfile(null, animate = false) + _taskLoadPolycentricProfile.run(video.author.id) when (video.rating) { is RatingLikeDislikes -> { - val r = video.rating as RatingLikeDislikes; - layoutRating.visibility = View.VISIBLE; + val r = video.rating as RatingLikeDislikes + layoutRating.visibility = VISIBLE - textLikes.visibility = View.VISIBLE; - imageLikeIcon.visibility = View.VISIBLE; - textLikes.text = r.likes.toHumanNumber(); + textLikes.visibility = VISIBLE + imageLikeIcon.visibility = VISIBLE + textLikes.text = r.likes.toHumanNumber() - imageDislikeIcon.visibility = View.VISIBLE; - textDislikes.visibility = View.VISIBLE; - textDislikes.text = r.dislikes.toHumanNumber(); + imageDislikeIcon.visibility = VISIBLE + textDislikes.visibility = VISIBLE + textDislikes.text = r.dislikes.toHumanNumber() } is RatingLikes -> { - val r = video.rating as RatingLikes; - layoutRating.visibility = View.VISIBLE; + val r = video.rating as RatingLikes + layoutRating.visibility = VISIBLE - textLikes.visibility = View.VISIBLE; - imageLikeIcon.visibility = View.VISIBLE; - textLikes.text = r.likes.toHumanNumber(); + textLikes.visibility = VISIBLE + imageLikeIcon.visibility = VISIBLE + textLikes.text = r.likes.toHumanNumber() - imageDislikeIcon.visibility = View.GONE; - textDislikes.visibility = View.GONE; + imageDislikeIcon.visibility = GONE + textDislikes.visibility = GONE } else -> { - layoutRating.visibility = View.GONE; + layoutRating.visibility = GONE } } monetization.onSupportTap.subscribe { - containerContentSupport.setPolycentricProfile(polycentricProfile); + containerContentSupport.setPolycentricProfile(polycentricProfile) animateOpenOverlayView(containerContentSupport) - }; + } monetization.onStoreTap.subscribe { polycentricProfile?.systemState?.store?.let { try { - val uri = Uri.parse(it); - val intent = Intent(Intent.ACTION_VIEW); - intent.data = uri; - requireContext().startActivity(intent); + val uri = Uri.parse(it) + val intent = Intent(Intent.ACTION_VIEW) + intent.data = uri + requireContext().startActivity(intent) } catch (e: Throwable) { - Logger.e(TAG, "Failed to open URI: '${it}'.", e); + Logger.e(TAG, "Failed to open URI: '${it}'.", e) } } - }; + } monetization.onUrlTap.subscribe { mainFragment!!.navigate(it) } @@ -1436,25 +1331,9 @@ class ShortView : ConstraintLayout { } channelButton.setOnClickListener { - mainFragment!!.navigate(video.author); - }; + mainFragment!!.navigate(video.author) + } -// composeView?.apply { -// setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) -// setContent { -// // In Compose world -// MaterialTheme { -// val view = LocalView.current -// IconButton(onClick = { -// view.playSoundEffect(SoundEffectConstants.CLICK) -// }) { -// Icon( -// Icons.Outlined.ThumbUp, contentDescription = "Close Bottom Sheet" -// ) -// } -// } -// } -// } return bottomSheetDialog } @@ -1466,15 +1345,15 @@ class ShortView : ConstraintLayout { private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) { polycentricProfile = profile - val dp_35 = 35.dp(requireContext().resources) - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35) + 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); + creatorThumbnail.setThumbnail(avatar, animate) } else { - creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate); - creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); + creatorThumbnail.setThumbnail(video.author.thumbnail, animate) + creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()) } val username = profile?.systemState?.username @@ -1482,7 +1361,7 @@ class ShortView : ConstraintLayout { channelName.text = username } - monetization.setPolycentricProfile(profile); + monetization.setPolycentricProfile(profile) } private fun setTabIndex(index: Int?, forceReload: Boolean = false) { @@ -1493,35 +1372,19 @@ class ShortView : ConstraintLayout { } tabIndex = index -// _buttonRecommended.setTextColor(resources.getColor(if (index == 2) R.color.white else R.color.gray_ac)) - buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac)) - buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac)) -// layoutRecommended.removeAllViews() + 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)) if (index == null) { - addCommentView.visibility = View.GONE + addCommentView.visibility = GONE commentsList.clear() -// _layoutRecommended.visibility = View.GONE } else if (index == 0) { - addCommentView.visibility = View.VISIBLE -// _layoutRecommended.visibility = View.GONE + addCommentView.visibility = VISIBLE fetchPolycentricComments() } else if (index == 1) { - addCommentView.visibility = View.GONE -// _layoutRecommended.visibility = View.GONE + addCommentView.visibility = GONE fetchComments() } -// else if (index == 2) { -// _addCommentView.visibility = View.GONE -// _layoutRecommended.visibility = View.VISIBLE -// _commentsList.clear() -// -// _layoutRecommended.addView(LoaderView(context).apply { -// layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources)) -// start() -// }) -// _taskLoadRecommendations.run(null) -// } } private fun fetchComments() { @@ -1533,9 +1396,9 @@ class ShortView : ConstraintLayout { private fun fetchPolycentricComments() { Logger.i(TAG, "fetchPolycentricComments") - val video = video; - val idValue = video?.id?.value - if (video?.url?.isEmpty() != false) { + val video = video + val idValue = video.id.value + if (video.url.isEmpty() != false) { Logger.w(TAG, "Failed to fetch polycentric comments because url was null") commentsList.clear() return @@ -1543,7 +1406,7 @@ class ShortView : ConstraintLayout { 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)); }; + commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); } } private fun updateDescriptionUI(text: Spanned) { 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 80322e5e..04eb3cba 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 @@ -16,11 +16,11 @@ import com.futo.platformplayer.R import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StatePlatform import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -43,9 +43,6 @@ class ShortsFragment : MainFragment() { private lateinit var overlayLoadingSpinner: ImageView private lateinit var overlayQualityContainer: FrameLayout private lateinit var customViewAdapter: CustomViewAdapter - private val urls = listOf( - "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://www.youtube.com/watch?v=MXHSS-7XcBc", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra", "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra" - ) init { loadPager() @@ -60,10 +57,10 @@ class ShortsFragment : MainFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewPager = view.findViewById(R.id.viewPager) + viewPager = view.findViewById(R.id.view_pager) overlayLoading = view.findViewById(R.id.short_view_loading_overlay) overlayLoadingSpinner = view.findViewById(R.id.short_view_loader) - overlayQualityContainer = view.findViewById(R.id.videodetail_quality_overview) + overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview) setLoading(true) @@ -72,12 +69,14 @@ class ShortsFragment : MainFragment() { } loadPagerJob!!.invokeOnCompletion { - customViewAdapter = CustomViewAdapter(videos, layoutInflater, this@ShortsFragment, overlayQualityContainer) { - if (!shortsPager!!.hasMorePages()) { - return@CustomViewAdapter + Logger.i(TAG, "Creating adapter") + customViewAdapter = + CustomViewAdapter(videos, layoutInflater, this@ShortsFragment, overlayQualityContainer) { + if (!shortsPager!!.hasMorePages()) { + return@CustomViewAdapter + } + nextPage() } - nextPage() - } customViewAdapter.onResetTriggered.subscribe { setLoading(true) loadPager() @@ -88,7 +87,6 @@ class ShortsFragment : MainFragment() { val viewPager = viewPager!! viewPager.adapter = customViewAdapter - // TODO something is laggy sometimes when swiping between videos viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { @OptIn(UnstableApi::class) override fun onPageSelected(position: Int) { @@ -96,7 +94,8 @@ class ShortsFragment : MainFragment() { adapter.previousShownView?.stop() adapter.previousShownView = null -// viewPager.post { + // the post prevents lag when swiping + viewPager.post { val recycler = (viewPager.getChildAt(0) as RecyclerView) val viewHolder = recycler.findViewHolderForAdapterPosition(position) as CustomViewHolder? @@ -108,10 +107,8 @@ class ShortsFragment : MainFragment() { focusedView.play() adapter.previousShownView = focusedView } -// } + } } - - }) setLoading(false) } @@ -154,12 +151,9 @@ class ShortsFragment : MainFragment() { viewPager?.currentItem = 0 loadPagerJob = CoroutineScope(Dispatchers.Main).launch { -// delay(5000) val pager = try { withContext(Dispatchers.IO) { StatePlatform.instance.getShorts() -// StatePlatform.instance.getHome() - // as IPager } } catch (_: CancellationException) { return@launch diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt index 27befb1e..0dba3c4f 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/WebviewOverlay.kt @@ -19,7 +19,9 @@ class WebviewOverlay : LinearLayout { inflate(context, R.layout.overlay_webview, this) _topbar = findViewById(R.id.topbar); _webview = findViewById(R.id.webview); - _webview.settings.javaScriptEnabled = true; + if (!isInEditMode){ + _webview.settings.javaScriptEnabled = true; + } _topbar.onClose.subscribe(this, onClose::emit); } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt index 8ff562b6..61cf886a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoShortPlayer.kt @@ -2,28 +2,25 @@ package com.futo.platformplayer.views.video import android.animation.ValueAnimator import android.content.Context -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater import android.view.animation.LinearInterpolator import androidx.annotation.OptIn +import androidx.media3.common.C import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.upstream.DefaultAllocator import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.TimeBar -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.api.media.models.video.IPlatformVideoDetails -import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StatePlayer +import com.futo.platformplayer.video.PlayerManager @UnstableApi class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : @@ -35,23 +32,9 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : } private var playerAttached = false -// private set; - private val videoView: PlayerView private val progressBar: DefaultTimeBar - - private val loadArtwork = object : CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - setArtwork(BitmapDrawable(resources, resource)) - } - - override fun onLoadCleared(placeholder: Drawable?) { - setArtwork(null) - } - } - - private val player = StatePlayer.instance.getShortPlayerOrCreate(context) - + private lateinit var player: PlayerManager private var progressAnimator: ValueAnimator = createProgressBarAnimator() private var playerEventListener = object : Player.Listener { @@ -67,10 +50,9 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : } if (player.isPlaying) { - if (progressAnimator.isPaused){ + if (progressAnimator.isPaused) { progressAnimator.resume() - } - else if (!progressAnimator.isStarted) { + } else if (!progressAnimator.isStarted) { progressAnimator.start() } } else { @@ -84,10 +66,13 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : init { LayoutInflater.from(context).inflate(R.layout.view_short_player, this, true) - videoView = findViewById(R.id.video_player) - progressBar = findViewById(R.id.video_player_progress_bar) + videoView = findViewById(R.id.short_player_view) + progressBar = findViewById(R.id.short_player_progress_bar) - player.player.repeatMode = Player.REPEAT_MODE_ONE + if (!isInEditMode) { + player = StatePlayer.instance.getShortPlayerOrCreate(context) + player.player.repeatMode = Player.REPEAT_MODE_ONE + } progressBar.addListener(object : TimeBar.OnScrubListener { override fun onScrubStart(timeBar: TimeBar, position: Long) { @@ -148,30 +133,6 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : player.detach() } - fun setPreview(video: IPlatformVideoDetails) { - if (video.live != null) { - setSource(video.live, null, play = true, keepSubtitles = false) - } else { - val videoSource = - VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS) - val audioSource = - VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context)) - if (videoSource == null && audioSource != null) { - val thumbnail = video.thumbnails.getHQThumbnail() - if (!thumbnail.isNullOrBlank()) { - Glide.with(videoView).asBitmap().load(thumbnail).into(loadArtwork) - } else { - Glide.with(videoView).clear(loadArtwork) - setArtwork(null) - } - } else { - Glide.with(videoView).clear(loadArtwork) - } - - setSource(videoSource, audioSource, play = true, keepSubtitles = false) - } - } - @OptIn(UnstableApi::class) fun setArtwork(drawable: Drawable?) { if (drawable != null) { @@ -194,9 +155,4 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) : val param = PlaybackParameters(playbackRate) exoPlayer?.playbackParameters = param } - - // TODO remove stub - fun hideControls(stub: Boolean) { - - } } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index c3eddb12..a7cf04e8 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -72,7 +72,7 @@ import kotlin.math.abs abstract class FutoVideoPlayerBase : ConstraintLayout { private val TAG = "FutoVideoPlayerBase" - private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory(); +// private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory(); private var _mediaSource: MediaSource? = null; diff --git a/app/src/main/res/layout/fragment_shorts.xml b/app/src/main/res/layout/fragment_shorts.xml index 7d6086da..25119232 100644 --- a/app/src/main/res/layout/fragment_shorts.xml +++ b/app/src/main/res/layout/fragment_shorts.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent"> @@ -29,7 +29,7 @@ - - + android:background="@color/black" + android:orientation="vertical" + tools:ignore="SpeakableTextPresentCheck"> + android:layout_height="wrap_content" + android:orientation="vertical"> + + android:orientation="horizontal"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + android:layout_marginStart="14dp" + android:layout_marginEnd="14dp"> - + + + + + - + - + android:orientation="horizontal"> - + + + + + + android:orientation="horizontal"> + + + + - - - - - - - - - - - - - - - - -