From 1ccae849330957ac56def6a8d6de3fc6ee4f3c02 Mon Sep 17 00:00:00 2001 From: Kai DeLorenzo Date: Tue, 28 May 2024 17:05:35 -0500 Subject: [PATCH 1/5] add support for channel playlists on the channel page --- .../channel/tab/ChannelPlaylistsFragment.kt | 353 ++++++++++++++++++ .../mainactivity/main/ChannelFragment.kt | 20 +- .../views/adapters/ChannelViewPagerAdapter.kt | 14 +- 3 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt new file mode 100644 index 00000000..a344c7da --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt @@ -0,0 +1,353 @@ +package com.futo.platformplayer.fragment.channel.tab + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.futo.platformplayer.R +import com.futo.platformplayer.Settings +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.api.media.models.PlatformAuthorLink +import com.futo.platformplayer.api.media.models.channels.IPlatformChannel +import com.futo.platformplayer.api.media.models.contents.ContentType +import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.api.media.platforms.js.models.JSPager +import com.futo.platformplayer.api.media.structures.IAsyncPager +import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.api.media.structures.IRefreshPager +import com.futo.platformplayer.api.media.structures.IReplacerPager +import com.futo.platformplayer.api.media.structures.MultiPager +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.constructs.Event2 +import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.engine.exceptions.PluginException +import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException +import com.futo.platformplayer.exceptions.ChannelException +import com.futo.platformplayer.fragment.mainactivity.main.FeedView +import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateCache +import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.states.StateSubscriptions +import com.futo.platformplayer.views.FeedStyle +import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder +import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader +import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { + private var _recyclerResults: RecyclerView? = null + private var _llmVideo: LinearLayoutManager? = null + private var _loading = false + private var _pagerParent: IPager? = null + private var _pager: IPager? = null + private var _cache: FeedView.ItemCache? = null + private var _channel: IPlatformChannel? = null + private var _results: ArrayList = arrayListOf() + private var _adapterResults: InsertedViewAdapterWithLoader? = null + private var _lastPolycentricProfile: PolycentricProfile? = null + + val onContentClicked = Event2() + val onContentUrlClicked = Event2() + val onUrlClicked = Event1() + val onChannelClicked = Event1() + val onAddToClicked = Event1() + val onAddToQueueClicked = Event1() + val onAddToWatchLaterClicked = Event1() + val onLongPress = Event1() + + private fun getPlaylistPager(channel: IPlatformChannel): IPager { + Logger.i(TAG, "getPlaylistPager") + + return StatePlatform.instance.getChannelPlaylists(channel.url) as IPager + } + + private val _taskLoadVideos = + TaskHandler>({ lifecycleScope }, { + val livePager = getPlaylistPager(it) + return@TaskHandler if (_channel?.let { channel -> + StateSubscriptions.instance.isSubscribed( + channel + ) + } == true) + StateCache.cachePagerResults(lifecycleScope, livePager) + else livePager + }).success { livePager -> + setLoading(false) + + setPager(livePager) + } + .exception { } + .exception { + Logger.w(TAG, "Failed to load initial videos.", it) + UIDialogs.showGeneralRetryErrorDialog( + requireContext(), + it.message ?: "", + it, + { loadNextPage() }) + } + + private var _nextPageHandler: TaskHandler, List> = + TaskHandler, List>({ lifecycleScope }, { + if (it is IAsyncPager<*>) + it.nextPageAsync() + else + it.nextPage() + + processPagerExceptions(it) + return@TaskHandler it.getResults() + }).success { + setLoading(false) + val posBefore = _results.size + //val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo } + _results.addAll(it) + _adapterResults?.let { adapterVideo -> + adapterVideo.notifyItemRangeInserted( + adapterVideo.childToParentPosition( + posBefore + ), it.size + ) + } + }.exception { + Logger.w(TAG, "Failed to load next page.", it) + UIDialogs.showGeneralRetryErrorDialog( + requireContext(), + it.message ?: "", + it, + { loadNextPage() }) + } + + private val _scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val recyclerResults = _recyclerResults ?: return + val llmVideo = _llmVideo ?: return + + val visibleItemCount = recyclerResults.childCount + val firstVisibleItem = llmVideo.findFirstVisibleItemPosition() + val visibleThreshold = 15 + if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size) { + loadNextPage() + } + } + } + + override fun setChannel(channel: IPlatformChannel) { + val c = _channel + if (c != null && c.url == channel.url) { + Logger.i(TAG, "setChannel skipped because previous was same") + return + } + + Logger.i(TAG, "setChannel setChannel=${channel}") + + _taskLoadVideos.cancel() + + _channel = channel + _results.clear() + _adapterResults?.notifyDataSetChanged() + + loadInitial() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_channel_videos, container, false) + + _recyclerResults = view.findViewById(R.id.recycler_videos) + + _adapterResults = PreviewContentListAdapter( + view.context, + FeedStyle.THUMBNAIL, + _results, + null, + Settings.instance.channel.progressBar + ).apply { + this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit) + this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit) + this.onContentClicked.subscribe(this@ChannelPlaylistsFragment.onContentClicked::emit) + this.onChannelClicked.subscribe(this@ChannelPlaylistsFragment.onChannelClicked::emit) + this.onAddToClicked.subscribe(this@ChannelPlaylistsFragment.onAddToClicked::emit) + this.onAddToQueueClicked.subscribe(this@ChannelPlaylistsFragment.onAddToQueueClicked::emit) + this.onAddToWatchLaterClicked.subscribe(this@ChannelPlaylistsFragment.onAddToWatchLaterClicked::emit) + this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit) + } + + _llmVideo = LinearLayoutManager(view.context) + _recyclerResults?.adapter = _adapterResults + _recyclerResults?.layoutManager = _llmVideo + _recyclerResults?.addOnScrollListener(_scrollListener) + + return view + } + + override fun onDestroyView() { + super.onDestroyView() + _recyclerResults?.removeOnScrollListener(_scrollListener) + _recyclerResults = null + _pager = null + + _taskLoadVideos.cancel() + _nextPageHandler.cancel() + } + + private fun setPager( + pager: IPager, + cache: FeedView.ItemCache? = null + ) { + if (_pagerParent != null && _pagerParent is IRefreshPager<*>) { + (_pagerParent as IRefreshPager<*>).onPagerError.remove(this) + (_pagerParent as IRefreshPager<*>).onPagerChanged.remove(this) + _pagerParent = null + } + if (_pager is IReplacerPager<*>) + (_pager as IReplacerPager<*>).onReplaced.remove(this) + + val pagerToSet: IPager? + if (pager is IRefreshPager<*>) { + _pagerParent = pager + pagerToSet = pager.getCurrentPager() as IPager + pager.onPagerChanged.subscribe(this) { + + lifecycleScope.launch(Dispatchers.Main) { + try { + loadPagerInternal(it as IPager) + } catch (e: Throwable) { + Logger.e(TAG, "loadPagerInternal failed.", e) + } + } + } + pager.onPagerError.subscribe(this) { + Logger.e(TAG, "Search pager failed: ${it.message}", it) + if (it is PluginException) + UIDialogs.toast("Plugin [${it.config.name}] failed due to:\n${it.message}") + else + UIDialogs.toast("Plugin failed due to:\n${it.message}") + } + } else pagerToSet = pager + + loadPagerInternal(pagerToSet, cache) + } + + private fun loadPagerInternal( + pager: IPager, + cache: FeedView.ItemCache? = null + ) { + _cache = cache + + if (_pager is IReplacerPager<*>) + (_pager as IReplacerPager<*>).onReplaced.remove(this) + if (pager is IReplacerPager<*>) { + pager.onReplaced.subscribe(this) { oldItem, newItem -> + if (_pager != pager) + return@subscribe + + lifecycleScope.launch(Dispatchers.Main) { + val toReplaceIndex = _results.indexOfFirst { it == oldItem } + if (toReplaceIndex >= 0) { + _results[toReplaceIndex] = newItem as IPlatformContent + _adapterResults?.let { + it.notifyItemChanged(it.childToParentPosition(toReplaceIndex)) + } + } + } + } + } + + _pager = pager + + processPagerExceptions(pager) + + _results.clear() + val toAdd = pager.getResults() + _results.addAll(toAdd) + _adapterResults?.notifyDataSetChanged() + _recyclerResults?.scrollToPosition(0) + } + + private fun loadInitial() { + val channel: IPlatformChannel = _channel ?: return + setLoading(true) + _taskLoadVideos.run(channel) + } + + private fun loadNextPage() { + val pager: IPager = _pager ?: return + if (_pager?.hasMorePages() == true) { + setLoading(true) + _nextPageHandler.run(pager) + } + } + + private fun setLoading(loading: Boolean) { + _loading = loading + _adapterResults?.setLoading(loading) + } + + fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { + val p = _lastPolycentricProfile + if (p != null && polycentricProfile != null && p.system == polycentricProfile.system) { + Logger.i( + ChannelContentsFragment.TAG, + "setPolycentricProfile skipped because previous was same" + ) + return + } + + _lastPolycentricProfile = polycentricProfile + + if (polycentricProfile != null) { + _taskLoadVideos.cancel() + val itemsRemoved = _results.size + _results.clear() + _adapterResults?.notifyItemRangeRemoved(0, itemsRemoved) + loadInitial() + } + } + + private fun processPagerExceptions(pager: IPager<*>) { + if (pager is MultiPager<*> && pager.allowFailure) { + val ex = pager.getResultExceptions() + for (kv in ex) { + val jsVideoPager: JSPager<*>? = when (kv.key) { + is MultiPager<*> -> (kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>? + is JSPager<*> -> kv.key as JSPager<*> + else -> null + } + + context?.let { + lifecycleScope.launch(Dispatchers.Main) { + try { + val channel = + if (kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null + if (jsVideoPager != null) + UIDialogs.toast( + it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n" + + (if (!channel.isNullOrEmpty()) "(${channel}) " else "") + + "${kv.value.message}", false + ) + else + UIDialogs.toast(it, kv.value.message ?: "", false) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to show toast.", e) + } + } + } + } + } + } + + companion object { + const val TAG = "PlaylistsFragment" + fun newInstance() = ChannelPlaylistsFragment().apply { } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index 3359119f..2edb7e44 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -32,6 +32,7 @@ import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment +import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger @@ -57,6 +58,8 @@ import kotlinx.serialization.Serializable @Serializable data class PolycentricProfile(val system: PublicKey, val systemState: SystemState, val ownedClaims: List); +const val PLAYLIST_POSITION = 4 + class ChannelFragment : MainFragment() { override val isMainView : Boolean = true; override val hasBottomBar: Boolean = true; @@ -241,6 +244,7 @@ class ChannelFragment : MainFragment() { //2 -> "STORE" 2 -> "SUPPORT" 3 -> "ABOUT" + PLAYLIST_POSITION -> "PLAYLISTS" else -> "Unknown $position" }; }; @@ -384,6 +388,10 @@ class ChannelFragment : MainFragment() { private fun showChannel(channel: IPlatformChannel) { setLoading(false); + if (!StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) { + _tabs.removeTabAt(PLAYLIST_POSITION) + } + _fragment.topBar?.onShown(channel); val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) { @@ -435,6 +443,10 @@ class ChannelFragment : MainFragment() { it.getFragment().setChannel(channel); it.getFragment().setChannel(channel); it.getFragment().setChannel(channel); + if (StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) { + Logger.w(TAG, "Supported channel playlists??"); + it.getFragment().setChannel(channel); + } //TODO: Call on other tabs as needed } @@ -501,6 +513,12 @@ class ChannelFragment : MainFragment() { it.getFragment().setPolycentricProfile(profile); it.getFragment().setPolycentricProfile(profile); it.getFragment().setPolycentricProfile(profile); + channel?.let { channel -> + if (StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) { + Logger.w(TAG, "Supported channel playlists??"); + it.getFragment().setPolycentricProfile(profile); + } + } //TODO: Call on other tabs as needed } } @@ -510,4 +528,4 @@ class ChannelFragment : MainFragment() { val TAG = "ChannelFragment"; fun newInstance() = ChannelFragment().apply { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt index de0f6c97..e908084b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt @@ -12,7 +12,7 @@ import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.fragment.channel.tab.* class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { - private val _cache: Array = arrayOfNulls(4); + private val _cache: Array = arrayOfNulls(5); val onContentUrlClicked = Event2(); val onUrlClicked = Event1(); @@ -39,6 +39,8 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec return createFragment(2) as T; else if(T::class == ChannelAboutFragment::class) return createFragment(3) as T; + else if(T::class == ChannelPlaylistsFragment::class) + return createFragment(4) as T; else throw NotImplementedError("Implement other types"); } @@ -64,6 +66,16 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec //2 -> ChannelStoreFragment.newInstance(); 2 -> ChannelMonetizationFragment.newInstance(); 3 -> ChannelAboutFragment.newInstance(); + 4 -> ChannelPlaylistsFragment.newInstance().apply { + onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit); + onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit); + onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit); + onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit); + onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit); + onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit); + onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit); + onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit); + }; else -> throw IllegalStateException("Invalid tab position $position") }; From 5edd389e841a9b12b42694702cdd898825a5f0ac Mon Sep 17 00:00:00 2001 From: Kai DeLorenzo Date: Tue, 4 Jun 2024 20:22:42 -0500 Subject: [PATCH 2/5] removed hardcoding. fixed bugs. hide CHANNELS and SUPPORT for non polycentric linked channels --- .../channel/tab/ChannelAboutFragment.kt | 2 +- .../channel/tab/ChannelContentsFragment.kt | 2 +- .../channel/tab/ChannelListFragment.kt | 2 +- .../tab/ChannelMonetizationFragment.kt | 2 +- .../channel/tab/ChannelPlaylistsFragment.kt | 154 ++-- .../channel/tab/IChannelTabFragment.kt | 8 +- .../mainactivity/main/ChannelFragment.kt | 658 ++++++++++-------- .../views/adapters/ChannelViewPagerAdapter.kt | 158 +++-- 8 files changed, 505 insertions(+), 481 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt index f7d9d9bf..19f1454b 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelAboutFragment.kt @@ -114,7 +114,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment { } - fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { + override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { _lastPolycentricProfile = polycentricProfile; if (polycentricProfile == null) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt index 0647afed..ea8ee4fa 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt @@ -309,7 +309,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment { _adapterResults?.setLoading(loading); } - fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { + override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { val p = _lastPolycentricProfile; if (p != null && polycentricProfile != null && p.system == polycentricProfile.system) { Logger.i(TAG, "setPolycentricProfile skipped because previous was same"); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt index 00ec5982..807fbd90 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelListFragment.kt @@ -124,7 +124,7 @@ class ChannelListFragment : Fragment, IChannelTabFragment { } } - fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { + override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { _taskLoadChannel.cancel(); _lastPolycentricProfile = polycentricProfile; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt index e2e39ee7..53268d16 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelMonetizationFragment.kt @@ -46,7 +46,7 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment { _lastChannel = channel; } - fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { + override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { _lastPolycentricProfile = polycentricProfile if (polycentricProfile != null) { _supportView?.setPolycentricProfile(polycentricProfile) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt index a344c7da..dbb58b68 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt @@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.platforms.js.models.JSPager import com.futo.platformplayer.api.media.structures.IAsyncPager import com.futo.platformplayer.api.media.structures.IPager @@ -27,12 +28,8 @@ import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.exceptions.ChannelException -import com.futo.platformplayer.fragment.mainactivity.main.FeedView -import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StatePlatform -import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader @@ -42,15 +39,13 @@ import kotlinx.coroutines.launch class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { private var _recyclerResults: RecyclerView? = null - private var _llmVideo: LinearLayoutManager? = null + private var _llmPlaylist: LinearLayoutManager? = null private var _loading = false - private var _pagerParent: IPager? = null - private var _pager: IPager? = null - private var _cache: FeedView.ItemCache? = null + private var _pagerParent: IPager? = null + private var _pager: IPager? = null private var _channel: IPlatformChannel? = null private var _results: ArrayList = arrayListOf() private var _adapterResults: InsertedViewAdapterWithLoader? = null - private var _lastPolycentricProfile: PolycentricProfile? = null val onContentClicked = Event2() val onContentUrlClicked = Event2() @@ -61,62 +56,49 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { val onAddToWatchLaterClicked = Event1() val onLongPress = Event1() - private fun getPlaylistPager(channel: IPlatformChannel): IPager { + private fun getPlaylistPager(channel: IPlatformChannel): IPager { Logger.i(TAG, "getPlaylistPager") - return StatePlatform.instance.getChannelPlaylists(channel.url) as IPager + return StatePlatform.instance.getChannelPlaylists(channel.url) } - private val _taskLoadVideos = - TaskHandler>({ lifecycleScope }, { + private val _taskLoadPlaylists = + TaskHandler>({ lifecycleScope }, { val livePager = getPlaylistPager(it) - return@TaskHandler if (_channel?.let { channel -> - StateSubscriptions.instance.isSubscribed( - channel - ) - } == true) - StateCache.cachePagerResults(lifecycleScope, livePager) - else livePager + return@TaskHandler livePager }).success { livePager -> setLoading(false) setPager(livePager) - } - .exception { } - .exception { - Logger.w(TAG, "Failed to load initial videos.", it) - UIDialogs.showGeneralRetryErrorDialog( - requireContext(), + }.exception { }.exception { + Logger.w(TAG, "Failed to load initial playlists.", it) + UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() }) } - private var _nextPageHandler: TaskHandler, List> = - TaskHandler, List>({ lifecycleScope }, { - if (it is IAsyncPager<*>) - it.nextPageAsync() - else - it.nextPage() + private var _nextPageHandler: TaskHandler, List> = + TaskHandler, List>({ lifecycleScope }, { + if (it is IAsyncPager<*>) it.nextPageAsync() + else it.nextPage() processPagerExceptions(it) return@TaskHandler it.getResults() }).success { setLoading(false) val posBefore = _results.size - //val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo } _results.addAll(it) - _adapterResults?.let { adapterVideo -> - adapterVideo.notifyItemRangeInserted( - adapterVideo.childToParentPosition( + _adapterResults?.let { adapterResult -> + adapterResult.notifyItemRangeInserted( + adapterResult.childToParentPosition( posBefore ), it.size ) } }.exception { Logger.w(TAG, "Failed to load next page.", it) - UIDialogs.showGeneralRetryErrorDialog( - requireContext(), + UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() }) @@ -127,10 +109,10 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { super.onScrolled(recyclerView, dx, dy) val recyclerResults = _recyclerResults ?: return - val llmVideo = _llmVideo ?: return + val llmPlaylist = _llmPlaylist ?: return val visibleItemCount = recyclerResults.childCount - val firstVisibleItem = llmVideo.findFirstVisibleItemPosition() + val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition() val visibleThreshold = 15 if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size) { loadNextPage() @@ -147,7 +129,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { Logger.i(TAG, "setChannel setChannel=${channel}") - _taskLoadVideos.cancel() + _taskLoadPlaylists.cancel() _channel = channel _results.clear() @@ -157,20 +139,14 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { } override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_channel_videos, container, false) _recyclerResults = view.findViewById(R.id.recycler_videos) _adapterResults = PreviewContentListAdapter( - view.context, - FeedStyle.THUMBNAIL, - _results, - null, - Settings.instance.channel.progressBar + view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar ).apply { this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit) this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit) @@ -182,9 +158,9 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit) } - _llmVideo = LinearLayoutManager(view.context) + _llmPlaylist = LinearLayoutManager(view.context) _recyclerResults?.adapter = _adapterResults - _recyclerResults?.layoutManager = _llmVideo + _recyclerResults?.layoutManager = _llmPlaylist _recyclerResults?.addOnScrollListener(_scrollListener) return view @@ -196,31 +172,29 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { _recyclerResults = null _pager = null - _taskLoadVideos.cancel() + _taskLoadPlaylists.cancel() _nextPageHandler.cancel() } private fun setPager( - pager: IPager, - cache: FeedView.ItemCache? = null + pager: IPager ) { if (_pagerParent != null && _pagerParent is IRefreshPager<*>) { (_pagerParent as IRefreshPager<*>).onPagerError.remove(this) (_pagerParent as IRefreshPager<*>).onPagerChanged.remove(this) _pagerParent = null } - if (_pager is IReplacerPager<*>) - (_pager as IReplacerPager<*>).onReplaced.remove(this) + if (_pager is IReplacerPager<*>) (_pager as IReplacerPager<*>).onReplaced.remove(this) - val pagerToSet: IPager? + val pagerToSet: IPager? if (pager is IRefreshPager<*>) { _pagerParent = pager - pagerToSet = pager.getCurrentPager() as IPager + pagerToSet = pager.getCurrentPager() as IPager pager.onPagerChanged.subscribe(this) { lifecycleScope.launch(Dispatchers.Main) { try { - loadPagerInternal(it as IPager) + loadPagerInternal(it as IPager) } catch (e: Throwable) { Logger.e(TAG, "loadPagerInternal failed.", e) } @@ -228,33 +202,26 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { } pager.onPagerError.subscribe(this) { Logger.e(TAG, "Search pager failed: ${it.message}", it) - if (it is PluginException) - UIDialogs.toast("Plugin [${it.config.name}] failed due to:\n${it.message}") - else - UIDialogs.toast("Plugin failed due to:\n${it.message}") + if (it is PluginException) UIDialogs.toast("Plugin [${it.config.name}] failed due to:\n${it.message}") + else UIDialogs.toast("Plugin failed due to:\n${it.message}") } } else pagerToSet = pager - loadPagerInternal(pagerToSet, cache) + loadPagerInternal(pagerToSet) } private fun loadPagerInternal( - pager: IPager, - cache: FeedView.ItemCache? = null + pager: IPager ) { - _cache = cache - - if (_pager is IReplacerPager<*>) - (_pager as IReplacerPager<*>).onReplaced.remove(this) + if (_pager is IReplacerPager<*>) (_pager as IReplacerPager<*>).onReplaced.remove(this) if (pager is IReplacerPager<*>) { pager.onReplaced.subscribe(this) { oldItem, newItem -> - if (_pager != pager) - return@subscribe + if (_pager != pager) return@subscribe lifecycleScope.launch(Dispatchers.Main) { val toReplaceIndex = _results.indexOfFirst { it == oldItem } if (toReplaceIndex >= 0) { - _results[toReplaceIndex] = newItem as IPlatformContent + _results[toReplaceIndex] = newItem as IPlatformPlaylist _adapterResults?.let { it.notifyItemChanged(it.childToParentPosition(toReplaceIndex)) } @@ -277,11 +244,11 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { private fun loadInitial() { val channel: IPlatformChannel = _channel ?: return setLoading(true) - _taskLoadVideos.run(channel) + _taskLoadPlaylists.run(channel) } private fun loadNextPage() { - val pager: IPager = _pager ?: return + val pager: IPager = _pager ?: return if (_pager?.hasMorePages() == true) { setLoading(true) _nextPageHandler.run(pager) @@ -293,32 +260,11 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { _adapterResults?.setLoading(loading) } - fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { - val p = _lastPolycentricProfile - if (p != null && polycentricProfile != null && p.system == polycentricProfile.system) { - Logger.i( - ChannelContentsFragment.TAG, - "setPolycentricProfile skipped because previous was same" - ) - return - } - - _lastPolycentricProfile = polycentricProfile - - if (polycentricProfile != null) { - _taskLoadVideos.cancel() - val itemsRemoved = _results.size - _results.clear() - _adapterResults?.notifyItemRangeRemoved(0, itemsRemoved) - loadInitial() - } - } - private fun processPagerExceptions(pager: IPager<*>) { if (pager is MultiPager<*> && pager.allowFailure) { val ex = pager.getResultExceptions() for (kv in ex) { - val jsVideoPager: JSPager<*>? = when (kv.key) { + val jsPager: JSPager<*>? = when (kv.key) { is MultiPager<*> -> (kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>? is JSPager<*> -> kv.key as JSPager<*> else -> null @@ -329,14 +275,12 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { try { val channel = if (kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null - if (jsVideoPager != null) - UIDialogs.toast( - it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n" + - (if (!channel.isNullOrEmpty()) "(${channel}) " else "") + - "${kv.value.message}", false - ) - else - UIDialogs.toast(it, kv.value.message ?: "", false) + if (jsPager != null) UIDialogs.toast( + it, + "Plugin ${jsPager.getPluginConfig().name} failed:\n" + (if (!channel.isNullOrEmpty()) "(${channel}) " else "") + "${kv.value.message}", + false + ) + else UIDialogs.toast(it, kv.value.message ?: "", false) } catch (e: Throwable) { Logger.e(TAG, "Failed to show toast.", e) } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt index e1da77c5..2b615d25 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/IChannelTabFragment.kt @@ -1,7 +1,11 @@ package com.futo.platformplayer.fragment.channel.tab import com.futo.platformplayer.api.media.models.channels.IPlatformChannel +import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile interface IChannelTabFragment { - fun setChannel(channel: IPlatformChannel); -} \ No newline at end of file + fun setChannel(channel: IPlatformChannel) + fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { + + } +} diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index 2edb7e44..f6126c58 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -15,8 +15,9 @@ import androidx.appcompat.widget.AppCompatImageView import androidx.lifecycle.lifecycleScope import androidx.viewpager2.widget.ViewPager2 import com.bumptech.glide.Glide -import com.futo.platformplayer.* import com.futo.platformplayer.R +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.models.PlatformAuthorLink @@ -27,27 +28,32 @@ import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.post.IPlatformPost import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo +import com.futo.platformplayer.assume import com.futo.platformplayer.constructs.TaskHandler -import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment -import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment -import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment -import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment -import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment +import com.futo.platformplayer.dp +import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.SearchType import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.polycentric.PolycentricCache +import com.futo.platformplayer.selectBestImage +import com.futo.platformplayer.selectHighestResolutionImage import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StateSubscriptions +import com.futo.platformplayer.toHumanNumber +import com.futo.platformplayer.views.adapters.ChannelTab import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.subscriptions.SubscribeButton -import com.futo.polycentric.core.* +import com.futo.polycentric.core.OwnedClaim +import com.futo.polycentric.core.PublicKey +import com.futo.polycentric.core.SystemState +import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.Dispatchers @@ -56,476 +62,510 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable @Serializable -data class PolycentricProfile(val system: PublicKey, val systemState: SystemState, val ownedClaims: List); - -const val PLAYLIST_POSITION = 4 +data class PolycentricProfile( + val system: PublicKey, val systemState: SystemState, val ownedClaims: List +) class ChannelFragment : MainFragment() { - override val isMainView : Boolean = true; - override val hasBottomBar: Boolean = true; - private var _view: ChannelView? = null; + override val isMainView: Boolean = true + override val hasBottomBar: Boolean = true + private var _view: ChannelView? = null override fun onShownWithView(parameter: Any?, isBack: Boolean) { - super.onShownWithView(parameter, isBack); - _view?.onShown(parameter, isBack); + super.onShownWithView(parameter, isBack) + _view?.onShown(parameter, isBack) } - override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val view = ChannelView(this, inflater); - _view = view; - return view; + override fun onCreateMainView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + val view = ChannelView(this, inflater) + _view = view + return view } override fun onBackPressed(): Boolean { - return _view?.onBackPressed() ?: false; + return _view?.onBackPressed() ?: false } override fun onDestroyMainView() { - super.onDestroyMainView(); + super.onDestroyMainView() - _view?.cleanup(); - _view = null; + _view?.cleanup() + _view = null } fun selectTab(selectedTabIndex: Int) { - _view?.selectTab(selectedTabIndex); + _view?.selectTab(selectedTabIndex) } @SuppressLint("ViewConstructor") - class ChannelView : LinearLayout { - private val _fragment: ChannelFragment; + class ChannelView + (fragment: ChannelFragment, inflater: LayoutInflater) : LinearLayout(inflater.context) { + private val _fragment: ChannelFragment = fragment - private var _textChannel: TextView; - private var _textChannelSub: TextView; - private var _creatorThumbnail: CreatorThumbnail; - private var _imageBanner: AppCompatImageView; + private var _textChannel: TextView + private var _textChannelSub: TextView + private var _creatorThumbnail: CreatorThumbnail + private var _imageBanner: AppCompatImageView - private var _tabs: TabLayout; - private var _viewPager: ViewPager2; - private var _tabLayoutMediator: TabLayoutMediator; - private var _buttonSubscribe: SubscribeButton; - private var _buttonSubscriptionSettings: ImageButton; + private var _tabs: TabLayout + private var _viewPager: ViewPager2 - private var _overlayContainer: FrameLayout; - private var _overlay_loading: LinearLayout; - private var _overlay_loading_spinner: ImageView; + // private var _adapter: ChannelViewPagerAdapter; + private var _tabLayoutMediator: TabLayoutMediator + private var _buttonSubscribe: SubscribeButton + private var _buttonSubscriptionSettings: ImageButton - private var _slideUpOverlay: SlideUpMenuOverlay? = null; + private var _overlayContainer: FrameLayout + private var _overlayLoading: LinearLayout + private var _overlayLoadingSpinner: ImageView - private var _isLoading: Boolean = false; - private var _selectedTabIndex: Int = -1; + private var _slideUpOverlay: SlideUpMenuOverlay? = null + + private var _isLoading: Boolean = false + private var _selectedTabIndex: Int = -1 var channel: IPlatformChannel? = null - private set; - private var _url: String? = null; + private set + private var _url: String? = null - private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() { - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - super.onPageScrolled(position, positionOffset, positionOffsetPixels); - //recalculate(position, positionOffset); - } - } + private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {} - private val _taskLoadPolycentricProfile: TaskHandler; - private val _taskGetChannel: TaskHandler; + private val _taskLoadPolycentricProfile: TaskHandler + private val _taskGetChannel: TaskHandler - constructor(fragment: ChannelFragment, inflater: LayoutInflater) : super(inflater.context) { - _fragment = fragment; - inflater.inflate(R.layout.fragment_channel, this); - - _taskLoadPolycentricProfile = TaskHandler({fragment.lifecycleScope}, { id -> - return@TaskHandler PolycentricCache.instance.getProfileAsync(id); - }) - .success { it -> setPolycentricProfile(it, animate = true) } - .exception { - Logger.w(TAG, "Failed to load polycentric profile.", it); - }; - - _taskGetChannel = TaskHandler({fragment.lifecycleScope}, { url -> StatePlatform.instance.getChannelLive(url) }) - .success { showChannel(it); } + init { + inflater.inflate(R.layout.fragment_channel, this) + _taskLoadPolycentricProfile = + TaskHandler({ fragment.lifecycleScope }, + { id -> + return@TaskHandler PolycentricCache.instance.getProfileAsync(id) + }).success { setPolycentricProfile(it, animate = true) }.exception { + Logger.w(TAG, "Failed to load polycentric profile.", it) + } + _taskGetChannel = TaskHandler({ fragment.lifecycleScope }, + { url -> StatePlatform.instance.getChannelLive(url) }).success { showChannel(it); } .exception { - UIDialogs.showDialog(context, + UIDialogs.showDialog( + context, R.drawable.ic_sources, - context.getString(R.string.no_source_enabled_to_support_this_channel) + "\n(${_url})", null, null, + context.getString(R.string.no_source_enabled_to_support_this_channel) + "\n(${_url})", + null, + null, 0, UIDialogs.Action("Back", { - fragment.close(true); + fragment.close(true) }, UIDialogs.ActionStyle.PRIMARY) - ); + ) + }.exception { + Logger.e(TAG, "Failed to load channel.", it) + UIDialogs.showGeneralRetryErrorDialog( + context, it.message ?: "", it, { loadChannel() }, null, fragment + ) } - .exception { - Logger.e(TAG, "Failed to load channel.", it); - UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() }, null, fragment); - } - - val tabs: TabLayout = findViewById(R.id.tabs); - val viewPager: ViewPager2 = findViewById(R.id.view_pager); - _textChannel = findViewById(R.id.text_channel_name); - _textChannelSub = findViewById(R.id.text_metadata); - _creatorThumbnail = findViewById(R.id.creator_thumbnail); - _imageBanner = findViewById(R.id.image_channel_banner); - _buttonSubscribe = findViewById(R.id.button_subscribe); - _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings); - _overlay_loading = findViewById(R.id.channel_loading_overlay); - _overlay_loading_spinner = findViewById(R.id.channel_loader); - _overlayContainer = findViewById(R.id.overlay_container); - + val tabs: TabLayout = findViewById(R.id.tabs) + val viewPager: ViewPager2 = findViewById(R.id.view_pager) + _textChannel = findViewById(R.id.text_channel_name) + _textChannelSub = findViewById(R.id.text_metadata) + _creatorThumbnail = findViewById(R.id.creator_thumbnail) + _imageBanner = findViewById(R.id.image_channel_banner) + _buttonSubscribe = findViewById(R.id.button_subscribe) + _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings) + _overlayLoading = findViewById(R.id.channel_loading_overlay) + _overlayLoadingSpinner = findViewById(R.id.channel_loader) + _overlayContainer = findViewById(R.id.overlay_container) _buttonSubscribe.onSubscribed.subscribe { - UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer); - _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; + UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer) + _buttonSubscriptionSettings.visibility = + if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE } _buttonSubscribe.onUnSubscribed.subscribe { - _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; + _buttonSubscriptionSettings.visibility = + if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE } - _buttonSubscriptionSettings.setOnClickListener { - val url = channel?.url ?: _url ?: return@setOnClickListener; - val sub = StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener; - UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer); - }; + val url = channel?.url ?: _url ?: return@setOnClickListener + val sub = + StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener + UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer) + } //TODO: Determine if this is really the only solution (isSaveEnabled=false) - viewPager.isSaveEnabled = false; - viewPager.registerOnPageChangeCallback(_onPageChangeCallback); - val adapter = ChannelViewPagerAdapter(fragment.childFragmentManager, fragment.lifecycle); + viewPager.isSaveEnabled = false + viewPager.registerOnPageChangeCallback(_onPageChangeCallback) + val adapter = ChannelViewPagerAdapter(fragment.childFragmentManager, fragment.lifecycle) adapter.onChannelClicked.subscribe { c -> fragment.navigate(c) } adapter.onContentClicked.subscribe { v, _ -> - if(v is IPlatformVideo) { - StatePlayer.instance.clearQueue(); - fragment.navigate(v).maximizeVideoDetail(); - } else if (v is IPlatformPlaylist) { - fragment.navigate(v); - } else if (v is IPlatformPost) { - fragment.navigate(v); + when (v) { + is IPlatformVideo -> { + StatePlayer.instance.clearQueue() + fragment.navigate(v).maximizeVideoDetail() + } + + is IPlatformPlaylist -> { + fragment.navigate(v) + } + + is IPlatformPost -> { + fragment.navigate(v) + } } } - adapter.onAddToClicked.subscribe {content -> + adapter.onAddToClicked.subscribe { content -> _overlayContainer.let { - if(content is IPlatformVideo) - _slideUpOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it); + if (content is IPlatformVideo) _slideUpOverlay = + UISlideOverlays.showVideoOptionsOverlay(content, it) } } adapter.onAddToQueueClicked.subscribe { content -> - if(content is IPlatformVideo) { - StatePlayer.instance.addToQueue(content); + if (content is IPlatformVideo) { + StatePlayer.instance.addToQueue(content) } } adapter.onAddToWatchLaterClicked.subscribe { content -> - if(content is IPlatformVideo) { - StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content)); - UIDialogs.toast("Added to watch later\n[${content.name}]"); + if (content is IPlatformVideo) { + StatePlaylists.instance.addToWatchLater( + SerializedPlatformVideo.fromVideo( + content + ) + ) + UIDialogs.toast("Added to watch later\n[${content.name}]") } } adapter.onUrlClicked.subscribe { url -> - fragment.navigate(url); + fragment.navigate(url) } adapter.onContentUrlClicked.subscribe { url, contentType -> - when(contentType) { + when (contentType) { ContentType.MEDIA -> { - StatePlayer.instance.clearQueue(); - fragment.navigate(url).maximizeVideoDetail(); - }; - ContentType.URL -> fragment.navigate(url); - else -> {}; + StatePlayer.instance.clearQueue() + fragment.navigate(url).maximizeVideoDetail() + } + + ContentType.URL -> fragment.navigate(url) + else -> {} } } adapter.onLongPress.subscribe { content -> _overlayContainer.let { - if(content is IPlatformVideo) - _slideUpOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it); + if (content is IPlatformVideo) _slideUpOverlay = + UISlideOverlays.showVideoOptionsOverlay(content, it) } } - viewPager.adapter = adapter; - - val tabLayoutMediator = TabLayoutMediator(tabs, viewPager) { tab, position -> - tab.text = when (position) { - 0 -> "VIDEOS" - 1 -> "CHANNELS" - //2 -> "STORE" - 2 -> "SUPPORT" - 3 -> "ABOUT" - PLAYLIST_POSITION -> "PLAYLISTS" - else -> "Unknown $position" - }; - }; - tabLayoutMediator.attach(); - - _tabLayoutMediator = tabLayoutMediator; - _tabs = tabs; - _viewPager = viewPager; + viewPager.adapter = adapter + val tabLayoutMediator = TabLayoutMediator( + tabs, viewPager, (viewPager.adapter as ChannelViewPagerAdapter)::getTabNames + ) + tabLayoutMediator.attach() + _tabLayoutMediator = tabLayoutMediator + _tabs = tabs + _viewPager = viewPager if (_selectedTabIndex != -1) { - selectTab(_selectedTabIndex); + selectTab(_selectedTabIndex) } - - setLoading(true); + setLoading(true) } fun cleanup() { - _taskLoadPolycentricProfile.cancel(); - _taskGetChannel.cancel(); - _tabLayoutMediator.detach(); - _viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback); - hideSlideUpOverlay(); - (_overlay_loading_spinner.drawable as Animatable?)?.stop(); + _taskLoadPolycentricProfile.cancel() + _taskGetChannel.cancel() + _tabLayoutMediator.detach() + _viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback) + hideSlideUpOverlay() + (_overlayLoadingSpinner.drawable as Animatable?)?.stop() } fun onShown(parameter: Any?, isBack: Boolean) { - hideSlideUpOverlay(); - _taskLoadPolycentricProfile.cancel(); - _selectedTabIndex = -1; + hideSlideUpOverlay() + _taskLoadPolycentricProfile.cancel() + _selectedTabIndex = -1 if (!isBack || _url == null) { - _imageBanner.setImageDrawable(null); + _imageBanner.setImageDrawable(null) - if (parameter is String) { - _buttonSubscribe.setSubscribeChannel(parameter); - _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; - setPolycentricProfileOr(parameter) { - _textChannel.text = ""; - _textChannelSub.text = ""; - _creatorThumbnail.setThumbnail(null, true); - Glide.with(_imageBanner) - .clear(_imageBanner); - }; + when (parameter) { + is String -> { + _buttonSubscribe.setSubscribeChannel(parameter) + _buttonSubscriptionSettings.visibility = + if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE + setPolycentricProfileOr(parameter) { + _textChannel.text = "" + _textChannelSub.text = "" + _creatorThumbnail.setThumbnail(null, true) + Glide.with(_imageBanner).clear(_imageBanner) + } - _url = parameter; - loadChannel(); - } else if (parameter is SerializedChannel) { - showChannel(parameter); - _url = parameter.url; - loadChannel(); - } else if (parameter is IPlatformChannel) - showChannel(parameter); - else if (parameter is PlatformAuthorLink) { - setPolycentricProfileOr(parameter.url) { - _textChannel.text = parameter.name; - _textChannelSub.text = ""; - _creatorThumbnail.setThumbnail(parameter.thumbnail, true); - Glide.with(_imageBanner) - .clear(_imageBanner); + _url = parameter + loadChannel() + } - loadPolycentricProfile(parameter.id, parameter.url) - }; + is SerializedChannel -> { + showChannel(parameter) + _url = parameter.url + loadChannel() + } - _url = parameter.url; - loadChannel(); - } else if (parameter is Subscription) { - setPolycentricProfileOr(parameter.channel.url) { - _textChannel.text = parameter.channel.name; - _textChannelSub.text = ""; - _creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true); - Glide.with(_imageBanner) - .clear(_imageBanner); + is IPlatformChannel -> showChannel(parameter) + is PlatformAuthorLink -> { + setPolycentricProfileOr(parameter.url) { + _textChannel.text = parameter.name + _textChannelSub.text = "" + _creatorThumbnail.setThumbnail(parameter.thumbnail, true) + Glide.with(_imageBanner).clear(_imageBanner) - loadPolycentricProfile(parameter.channel.id, parameter.channel.url) - }; + loadPolycentricProfile(parameter.id, parameter.url) + } - _url = parameter.channel.url; - loadChannel(); + _url = parameter.url + loadChannel() + } + + is Subscription -> { + setPolycentricProfileOr(parameter.channel.url) { + _textChannel.text = parameter.channel.name + _textChannelSub.text = "" + _creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true) + Glide.with(_imageBanner).clear(_imageBanner) + + loadPolycentricProfile(parameter.channel.id, parameter.channel.url) + } + + _url = parameter.channel.url + loadChannel() + } } } else { - loadChannel(); + loadChannel() } } fun selectTab(selectedTabIndex: Int) { - _selectedTabIndex = selectedTabIndex; - _tabs.selectTab(_tabs.getTabAt(selectedTabIndex)); + _selectedTabIndex = selectedTabIndex + _tabs.selectTab(_tabs.getTabAt(selectedTabIndex)) } private fun loadPolycentricProfile(id: PlatformID, url: String) { - val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true); + val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true) if (cachedPolycentricProfile != null) { setPolycentricProfile(cachedPolycentricProfile, animate = true) if (cachedPolycentricProfile.expired) { - _taskLoadPolycentricProfile.run(id); + _taskLoadPolycentricProfile.run(id) } } else { - _taskLoadPolycentricProfile.run(id); + _taskLoadPolycentricProfile.run(id) } } private fun setLoading(isLoading: Boolean) { if (_isLoading == isLoading) { - return; + return } - _isLoading = isLoading; - if(isLoading){ - _overlay_loading.visibility = View.VISIBLE; - (_overlay_loading_spinner.drawable as Animatable?)?.start(); - } - else { - (_overlay_loading_spinner.drawable as Animatable?)?.stop(); - _overlay_loading.visibility = View.GONE; + _isLoading = isLoading + if (isLoading) { + _overlayLoading.visibility = View.VISIBLE + (_overlayLoadingSpinner.drawable as Animatable?)?.start() + } else { + (_overlayLoadingSpinner.drawable as Animatable?)?.stop() + _overlayLoading.visibility = View.GONE } } fun onBackPressed(): Boolean { if (_slideUpOverlay != null) { - hideSlideUpOverlay(); - return true; + hideSlideUpOverlay() + return true } - return false; + return false } private fun hideSlideUpOverlay() { - _slideUpOverlay?.hide(false); - _slideUpOverlay = null; + _slideUpOverlay?.hide(false) + _slideUpOverlay = null } private fun loadChannel() { - val url = _url; + val url = _url if (url != null) { - setLoading(true); - _taskGetChannel.run(url); + setLoading(true) + _taskGetChannel.run(url) } } private fun showChannel(channel: IPlatformChannel) { - setLoading(false); + setLoading(false) - if (!StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) { - _tabs.removeTabAt(PLAYLIST_POSITION) - } - - _fragment.topBar?.onShown(channel); + _fragment.topBar?.onShown(channel) val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) { - UIDialogs.showConfirmationDialog(context, context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist).replace("{channelName}", channel.name), { - UIDialogs.showDialogProgress(context) { - _fragment.lifecycleScope.launch(Dispatchers.IO) { - try { - StatePlaylists.instance.createPlaylistFromChannel(channel) { page -> - _fragment.lifecycleScope.launch(Dispatchers.Main) { - it.setText("${channel.name}\n" + context.getString(R.string.page) + " $page"); + UIDialogs.showConfirmationDialog(context, + context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist) + .replace("{channelName}", channel.name), + { + UIDialogs.showDialogProgress(context) { + _fragment.lifecycleScope.launch(Dispatchers.IO) { + try { + StatePlaylists.instance.createPlaylistFromChannel(channel) { page -> + _fragment.lifecycleScope.launch(Dispatchers.Main) { + it.setText("${channel.name}\n" + context.getString(R.string.page) + " $page") + } } - }; - } - catch(ex: Exception) { - Logger.e(TAG, "Error", ex); - UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_convert_channel), ex); - } + } catch (ex: Exception) { + Logger.e(TAG, "Error", ex) + UIDialogs.showGeneralErrorDialog( + context, + context.getString(R.string.failed_to_convert_channel), + ex + ) + } - withContext(Dispatchers.Main) { - it.hide(); + withContext(Dispatchers.Main) { + it.hide() + } } - }; - }; - }); - }); + } + }) + }) _fragment.lifecycleScope.launch(Dispatchers.IO) { - val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url); + val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url) withContext(Dispatchers.Main) { if (plugin != null && plugin.capabilities.hasSearchChannelContents) { buttons.add(Pair(R.drawable.ic_search) { - _fragment.navigate(SuggestionsFragmentData("", SearchType.VIDEO, channel.url)); - }); + _fragment.navigate( + SuggestionsFragmentData( + "", SearchType.VIDEO, channel.url + ) + ) + }) - _fragment.topBar?.assume()?.setMenuItems(buttons); + _fragment.topBar?.assume()?.setMenuItems(buttons) } } } - _buttonSubscribe.setSubscribeChannel(channel); - _buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; - _textChannel.text = channel.name; - _textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else ""; + _buttonSubscribe.setSubscribeChannel(channel) + _buttonSubscriptionSettings.visibility = + if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE + _textChannel.text = channel.name + _textChannelSub.text = + if (channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString( + R.string.subscribers + ).lowercase() else "" - //TODO: Find a better way to access the adapter fragments.. - - (_viewPager.adapter as ChannelViewPagerAdapter?)?.let { - it.getFragment().setChannel(channel); - it.getFragment().setChannel(channel); - it.getFragment().setChannel(channel); - it.getFragment().setChannel(channel); - if (StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) { - Logger.w(TAG, "Supported channel playlists??"); - it.getFragment().setChannel(channel); - } - //TODO: Call on other tabs as needed + val supportsPlaylists = + StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists + if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( + ChannelTab.PLAYLISTS.ordinal.toLong() + ) + ) { + (_viewPager.adapter as ChannelViewPagerAdapter).insert(2, ChannelTab.PLAYLISTS) + } + if (!supportsPlaylists && (_viewPager.adapter as ChannelViewPagerAdapter).containsItem( + ChannelTab.PLAYLISTS.ordinal.toLong() + ) + ) { + (_viewPager.adapter as ChannelViewPagerAdapter).remove(2) } - this.channel = channel; + // sets the channel for each tab + for (fragment in _fragment.childFragmentManager.fragments) { + (fragment as IChannelTabFragment).setChannel(channel) + } + + (_viewPager.adapter as ChannelViewPagerAdapter).channel = channel + + + _viewPager.adapter!!.notifyDataSetChanged() + + this.channel = channel setPolycentricProfileOr(channel.url) { - _textChannel.text = channel.name; - _creatorThumbnail.setThumbnail(channel.thumbnail, true); - Glide.with(_imageBanner) - .load(channel.banner) - .crossfade() - .into(_imageBanner); + _textChannel.text = channel.name + _creatorThumbnail.setThumbnail(channel.thumbnail, true) + Glide.with(_imageBanner).load(channel.banner).crossfade().into(_imageBanner) - _taskLoadPolycentricProfile.run(channel.id); - }; + _taskLoadPolycentricProfile.run(channel.id) + } } private fun setPolycentricProfileOr(url: String, or: () -> Unit) { - setPolycentricProfile(null, animate = false); + setPolycentricProfile(null, animate = false) - val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }; + val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) } if (cachedProfile != null) { - setPolycentricProfile(cachedProfile, animate = false); + setPolycentricProfile(cachedProfile, animate = false) } else { - or(); + or() } } - private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { - val dp_35 = 35.dp(resources) - val profile = cachedPolycentricProfile?.profile; - val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35) - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; - - if (avatar != null) { - _creatorThumbnail.setThumbnail(avatar, animate); - } else { - _creatorThumbnail.setThumbnail(channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); + private fun setPolycentricProfile( + cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean + ) { + val dp35 = 35.dp(resources) + val profile = cachedPolycentricProfile?.profile + val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let { + it.toURLInfoSystemLinkUrl( + profile.system.toProto(), it.process, profile.systemState.servers.toList() + ) } - val banner = profile?.systemState?.banner?.selectHighestResolutionImage() - ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; + if (avatar != null) { + _creatorThumbnail.setThumbnail(avatar, animate) + } else { + _creatorThumbnail.setThumbnail(channel?.thumbnail, animate) + _creatorThumbnail.setHarborAvailable( + profile != null, animate, profile?.system?.toProto() + ) + } + + val banner = profile?.systemState?.banner?.selectHighestResolutionImage()?.let { + it.toURLInfoSystemLinkUrl( + profile.system.toProto(), it.process, profile.systemState.servers.toList() + ) + } if (banner != null) { - Glide.with(_imageBanner) - .load(banner) - .crossfade() - .into(_imageBanner); + Glide.with(_imageBanner).load(banner).crossfade().into(_imageBanner) } else { - Glide.with(_imageBanner) - .load(channel?.banner) - .crossfade() - .into(_imageBanner); + Glide.with(_imageBanner).load(channel?.banner).crossfade().into(_imageBanner) } if (profile != null) { - _fragment.topBar?.onShown(profile); - _textChannel.text = profile.systemState.username; + _fragment.topBar?.onShown(profile) + _textChannel.text = profile.systemState.username } - (_viewPager.adapter as ChannelViewPagerAdapter?)?.let { - it.getFragment().setPolycentricProfile(profile); - it.getFragment().setPolycentricProfile(profile); - it.getFragment().setPolycentricProfile(profile); - it.getFragment().setPolycentricProfile(profile); - channel?.let { channel -> - if (StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) { - Logger.w(TAG, "Supported channel playlists??"); - it.getFragment().setPolycentricProfile(profile); - } - } - //TODO: Call on other tabs as needed + // sets the profile for each tab + for (fragment in _fragment.childFragmentManager.fragments) { + (fragment as IChannelTabFragment).setPolycentricProfile(profile) } + + //TODO only add channels and support if its setup on the polycentric profile + if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( + ChannelTab.SUPPORT.ordinal.toLong() + ) + ) { + (_viewPager.adapter as ChannelViewPagerAdapter).insert(2, ChannelTab.SUPPORT) + } + if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( + ChannelTab.CHANNELS.ordinal.toLong() + ) + ) { + (_viewPager.adapter as ChannelViewPagerAdapter).insert(2, ChannelTab.CHANNELS) + } + (_viewPager.adapter as ChannelViewPagerAdapter).profile = profile + _viewPager.adapter!!.notifyDataSetChanged() } } companion object { - val TAG = "ChannelFragment"; + const val TAG = "ChannelFragment" fun newInstance() = ChannelFragment().apply { } } } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt index e908084b..e0618cdd 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt @@ -5,81 +5,117 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.viewpager2.adapter.FragmentStateAdapter import com.futo.platformplayer.api.media.models.PlatformAuthorLink +import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 -import com.futo.platformplayer.fragment.channel.tab.* +import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment +import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment +import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment +import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment +import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment +import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment +import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile +import com.google.android.material.tabs.TabLayout -class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { - private val _cache: Array = arrayOfNulls(5); - val onContentUrlClicked = Event2(); - val onUrlClicked = Event1(); - val onContentClicked = Event2(); - val onChannelClicked = Event1(); - val onAddToClicked = Event1(); - val onAddToQueueClicked = Event1(); - val onAddToWatchLaterClicked = Event1(); - val onLongPress = Event1(); +enum class ChannelTab { + VIDEOS, CHANNELS, PLAYLISTS, SUPPORT, ABOUT +} + +class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : + FragmentStateAdapter(fragmentManager, lifecycle) { + private val _supportedFragments = mutableMapOf( + ChannelTab.VIDEOS.ordinal to ChannelTab.VIDEOS, ChannelTab.ABOUT.ordinal to ChannelTab.ABOUT + ) + private val _tabs = arrayListOf(ChannelTab.VIDEOS, ChannelTab.ABOUT) + + var profile: PolycentricProfile? = null + var channel: IPlatformChannel? = null + + val onContentUrlClicked = Event2() + val onUrlClicked = Event1() + val onContentClicked = Event2() + val onChannelClicked = Event1() + val onAddToClicked = Event1() + val onAddToQueueClicked = Event1() + val onAddToWatchLaterClicked = Event1() + val onLongPress = Event1() + + override fun getItemId(position: Int): Long { + return _tabs[position].ordinal.toLong() + } + + override fun containsItem(itemId: Long): Boolean { + return _supportedFragments.containsKey(itemId.toInt()) + } override fun getItemCount(): Int { - return _cache.size; + return _supportedFragments.size } - inline fun getFragment(): T { - //TODO: I have a feeling this can somehow be synced with createFragment so only 1 mapping exists (without a Map<>) - if(T::class == ChannelContentsFragment::class) - return createFragment(0) as T; - else if(T::class == ChannelListFragment::class) - return createFragment(1) as T; - //else if(T::class == ChannelStoreFragment::class) - // return createFragment(2) as T; - else if(T::class == ChannelMonetizationFragment::class) - return createFragment(2) as T; - else if(T::class == ChannelAboutFragment::class) - return createFragment(3) as T; - else if(T::class == ChannelPlaylistsFragment::class) - return createFragment(4) as T; - else - throw NotImplementedError("Implement other types"); + fun getTabNames(tab: TabLayout.Tab, position: Int) { + tab.text = _tabs[position].name + } + + fun insert(position: Int, tab: ChannelTab) { + _supportedFragments[tab.ordinal] = tab + _tabs.add(position, tab) + notifyItemInserted(position) + } + + fun remove(position: Int) { + _supportedFragments.remove(_tabs[position].ordinal) + _tabs.removeAt(position) + notifyItemRemoved(position) } override fun createFragment(position: Int): Fragment { - val cachedFragment = _cache[position]; - if (cachedFragment != null) { - return cachedFragment; + val fragment: Fragment + when (_tabs[position]) { + ChannelTab.VIDEOS -> { + fragment = ChannelContentsFragment.newInstance().apply { + onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit) + onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit) + onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit) + onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit) + onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit) + onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit) + onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit) + onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit) + } + } + + ChannelTab.CHANNELS -> { + fragment = ChannelListFragment.newInstance() + .apply { onClickChannel.subscribe(onChannelClicked::emit) } + } + + ChannelTab.PLAYLISTS -> { + fragment = ChannelPlaylistsFragment.newInstance().apply { + onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit) + onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit) + onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit) + onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit) + onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit) + onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit) + onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit) + onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit) + } + } + + ChannelTab.SUPPORT -> { + fragment = ChannelMonetizationFragment.newInstance() + } + + ChannelTab.ABOUT -> { + fragment = ChannelAboutFragment.newInstance() + } } + channel?.let { (fragment as IChannelTabFragment).setChannel(it) } + profile?.let { (fragment as IChannelTabFragment).setPolycentricProfile(it) } - val fragment = when (position) { - 0 -> ChannelContentsFragment.newInstance().apply { - onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit); - onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit); - onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit); - onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit); - onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit); - onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit); - onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit); - onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit); - }; - 1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) }; - //2 -> ChannelStoreFragment.newInstance(); - 2 -> ChannelMonetizationFragment.newInstance(); - 3 -> ChannelAboutFragment.newInstance(); - 4 -> ChannelPlaylistsFragment.newInstance().apply { - onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit); - onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit); - onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit); - onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit); - onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit); - onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit); - onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit); - onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit); - }; - else -> throw IllegalStateException("Invalid tab position $position") - }; - - _cache[position]= fragment; - return fragment; + return fragment } } \ No newline at end of file From ef284ba51d05586fe5a85168bd484d4dc3acb0a9 Mon Sep 17 00:00:00 2001 From: Kai DeLorenzo Date: Wed, 5 Jun 2024 13:44:05 -0500 Subject: [PATCH 3/5] fixed tab changing when adding the playlist tab --- .../mainactivity/main/ChannelFragment.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index f6126c58..8015574e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -459,17 +459,31 @@ class ChannelFragment : MainFragment() { val supportsPlaylists = StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists + val playlistPosition = 2 if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( ChannelTab.PLAYLISTS.ordinal.toLong() ) ) { - (_viewPager.adapter as ChannelViewPagerAdapter).insert(2, ChannelTab.PLAYLISTS) + // keep the current tab selected + if (_viewPager.currentItem >= playlistPosition) { + _viewPager.setCurrentItem(_viewPager.currentItem + 1, false) + } + + (_viewPager.adapter as ChannelViewPagerAdapter).insert( + playlistPosition, + ChannelTab.PLAYLISTS + ) } if (!supportsPlaylists && (_viewPager.adapter as ChannelViewPagerAdapter).containsItem( ChannelTab.PLAYLISTS.ordinal.toLong() ) ) { - (_viewPager.adapter as ChannelViewPagerAdapter).remove(2) + // keep the current tab selected + if (_viewPager.currentItem >= playlistPosition) { + _viewPager.setCurrentItem(_viewPager.currentItem - 1, false) + } + + (_viewPager.adapter as ChannelViewPagerAdapter).remove(playlistPosition) } // sets the channel for each tab From 4cf3aabe89e285c4af0b6176be356329f32b12ec Mon Sep 17 00:00:00 2001 From: Kai DeLorenzo Date: Wed, 5 Jun 2024 18:57:43 -0500 Subject: [PATCH 4/5] removed additional hardcoding --- .../fragment/mainactivity/main/ChannelFragment.kt | 14 ++++++++++---- .../mainactivity/main/PostDetailFragment.kt | 3 ++- .../views/adapters/ChannelViewPagerAdapter.kt | 4 ++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index 8015574e..97165845 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -95,8 +95,8 @@ class ChannelFragment : MainFragment() { _view = null } - fun selectTab(selectedTabIndex: Int) { - _view?.selectTab(selectedTabIndex) + fun selectTab(tab: ChannelTab) { + _view?.selectTab(tab) } @SuppressLint("ViewConstructor") @@ -268,6 +268,10 @@ class ChannelFragment : MainFragment() { setLoading(true) } + fun selectTab(tab: ChannelTab) { + (_viewPager.adapter as ChannelViewPagerAdapter).getTabPosition(tab) + } + fun cleanup() { _taskLoadPolycentricProfile.cancel() _taskGetChannel.cancel() @@ -560,18 +564,20 @@ class ChannelFragment : MainFragment() { (fragment as IChannelTabFragment).setPolycentricProfile(profile) } + val insertPosition = 2 + //TODO only add channels and support if its setup on the polycentric profile if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( ChannelTab.SUPPORT.ordinal.toLong() ) ) { - (_viewPager.adapter as ChannelViewPagerAdapter).insert(2, ChannelTab.SUPPORT) + (_viewPager.adapter as ChannelViewPagerAdapter).insert(insertPosition, ChannelTab.SUPPORT) } if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( ChannelTab.CHANNELS.ordinal.toLong() ) ) { - (_viewPager.adapter as ChannelViewPagerAdapter).insert(2, ChannelTab.CHANNELS) + (_viewPager.adapter as ChannelViewPagerAdapter).insert(insertPosition, ChannelTab.CHANNELS) } (_viewPager.adapter as ChannelViewPagerAdapter).profile = profile _viewPager.adapter!!.notifyDataSetChanged() diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt index 22ad5e93..df09b741 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt @@ -41,6 +41,7 @@ import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNumber +import com.futo.platformplayer.views.adapters.ChannelTab import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView import com.futo.platformplayer.views.comments.AddCommentView import com.futo.platformplayer.views.others.CreatorThumbnail @@ -264,7 +265,7 @@ class PostDetailFragment : MainFragment { _buttonSupport.setOnClickListener { val author = _post?.author ?: _postOverview?.author; - author?.let { _fragment.navigate(it).selectTab(2); }; + author?.let { _fragment.navigate(it).selectTab(ChannelTab.SUPPORT); }; }; _buttonStore.setOnClickListener { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt index e0618cdd..8bb2b946 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/ChannelViewPagerAdapter.kt @@ -55,6 +55,10 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec return _supportedFragments.size } + fun getTabPosition(tab: ChannelTab): Int { + return _tabs.indexOf(tab) + } + fun getTabNames(tab: TabLayout.Tab, position: Int) { tab.text = _tabs[position].name } From ebb469342558b19f61f76d586a18567a584aff30 Mon Sep 17 00:00:00 2001 From: Kai DeLorenzo Date: Fri, 7 Jun 2024 09:53:19 -0500 Subject: [PATCH 5/5] adjust tab order --- .../fragment/mainactivity/main/ChannelFragment.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index 97165845..dc419673 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -345,7 +345,7 @@ class ChannelFragment : MainFragment() { } } - fun selectTab(selectedTabIndex: Int) { + private fun selectTab(selectedTabIndex: Int) { _selectedTabIndex = selectedTabIndex _tabs.selectTab(_tabs.getTabAt(selectedTabIndex)) } @@ -463,7 +463,7 @@ class ChannelFragment : MainFragment() { val supportsPlaylists = StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists - val playlistPosition = 2 + val playlistPosition = 1 if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( ChannelTab.PLAYLISTS.ordinal.toLong() ) @@ -564,7 +564,7 @@ class ChannelFragment : MainFragment() { (fragment as IChannelTabFragment).setPolycentricProfile(profile) } - val insertPosition = 2 + val insertPosition = 1 //TODO only add channels and support if its setup on the polycentric profile if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(