diff --git a/LICENSE.md b/LICENSE.md index f3d7e742..38414394 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -7,7 +7,7 @@ By using the software, you agree to all of the terms and conditions below. FUTO Holdings, Inc. (the “Licensor”) grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below. ## Limitations -You may use or modify the software for only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application. +You may use or modify the software only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application. You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes. 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 new file mode 100644 index 00000000..dbb58b68 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelPlaylistsFragment.kt @@ -0,0 +1,297 @@ +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.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 +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.logging.Logger +import com.futo.platformplayer.states.StatePlatform +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 _llmPlaylist: LinearLayoutManager? = null + private var _loading = false + 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 + + 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) + } + + private val _taskLoadPlaylists = + TaskHandler>({ lifecycleScope }, { + val livePager = getPlaylistPager(it) + return@TaskHandler livePager + }).success { livePager -> + setLoading(false) + + setPager(livePager) + }.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() + + processPagerExceptions(it) + return@TaskHandler it.getResults() + }).success { + setLoading(false) + val posBefore = _results.size + _results.addAll(it) + _adapterResults?.let { adapterResult -> + adapterResult.notifyItemRangeInserted( + adapterResult.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 llmPlaylist = _llmPlaylist ?: return + + val visibleItemCount = recyclerResults.childCount + val firstVisibleItem = llmPlaylist.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}") + + _taskLoadPlaylists.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) + } + + _llmPlaylist = LinearLayoutManager(view.context) + _recyclerResults?.adapter = _adapterResults + _recyclerResults?.layoutManager = _llmPlaylist + _recyclerResults?.addOnScrollListener(_scrollListener) + + return view + } + + override fun onDestroyView() { + super.onDestroyView() + _recyclerResults?.removeOnScrollListener(_scrollListener) + _recyclerResults = null + _pager = null + + _taskLoadPlaylists.cancel() + _nextPageHandler.cancel() + } + + private fun setPager( + 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) + + 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) + } + + private fun loadPagerInternal( + pager: IPager + ) { + 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 IPlatformPlaylist + _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) + _taskLoadPlaylists.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) + } + + private fun processPagerExceptions(pager: IPager<*>) { + if (pager is MultiPager<*> && pager.allowFailure) { + val ex = pager.getResultExceptions() + for (kv in ex) { + 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 + } + + context?.let { + lifecycleScope.launch(Dispatchers.Main) { + try { + val channel = + if (kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null + 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) + } + } + } + } + } + } + + 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/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 3359119f..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 @@ -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,26 +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.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 @@ -55,459 +62,530 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable @Serializable -data class PolycentricProfile(val system: PublicKey, val systemState: SystemState, val ownedClaims: List); +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); + fun selectTab(tab: ChannelTab) { + _view?.selectTab(tab) } @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" - 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 selectTab(tab: ChannelTab) { + (_viewPager.adapter as ChannelViewPagerAdapter).getTabPosition(tab) } 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)); + private fun selectTab(selectedTabIndex: Int) { + _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) - _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.. + val supportsPlaylists = + StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists + val playlistPosition = 1 + if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( + ChannelTab.PLAYLISTS.ordinal.toLong() + ) + ) { + // keep the current tab selected + if (_viewPager.currentItem >= playlistPosition) { + _viewPager.setCurrentItem(_viewPager.currentItem + 1, false) + } - (_viewPager.adapter as ChannelViewPagerAdapter?)?.let { - it.getFragment().setChannel(channel); - it.getFragment().setChannel(channel); - it.getFragment().setChannel(channel); - it.getFragment().setChannel(channel); - //TODO: Call on other tabs as needed + (_viewPager.adapter as ChannelViewPagerAdapter).insert( + playlistPosition, + ChannelTab.PLAYLISTS + ) + } + if (!supportsPlaylists && (_viewPager.adapter as ChannelViewPagerAdapter).containsItem( + ChannelTab.PLAYLISTS.ordinal.toLong() + ) + ) { + // keep the current tab selected + if (_viewPager.currentItem >= playlistPosition) { + _viewPager.setCurrentItem(_viewPager.currentItem - 1, false) + } + + (_viewPager.adapter as ChannelViewPagerAdapter).remove(playlistPosition) } - 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); - //TODO: Call on other tabs as needed + // sets the profile for each tab + for (fragment in _fragment.childFragmentManager.fragments) { + (fragment as IChannelTabFragment).setPolycentricProfile(profile) } + + val insertPosition = 1 + + //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(insertPosition, ChannelTab.SUPPORT) + } + if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem( + ChannelTab.CHANNELS.ordinal.toLong() + ) + ) { + (_viewPager.adapter as ChannelViewPagerAdapter).insert(insertPosition, ChannelTab.CHANNELS) + } + (_viewPager.adapter as ChannelViewPagerAdapter).profile = profile + _viewPager.adapter!!.notifyDataSetChanged() } } companion object { - val TAG = "ChannelFragment"; + const val TAG = "ChannelFragment" fun newInstance() = ChannelFragment().apply { } } -} \ No newline at end of file +} 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/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index 9ca4aa8e..df4cf4eb 100644 --- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt +++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt @@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource +import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource @@ -44,7 +45,7 @@ class VideoHelper { } fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource; - fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource || source is IHLSManifestAudioSource; + fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource) && source !is IAudioUrlWidevineSource fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers); fun selectBestVideoSource(sources: Iterable, desiredPixelCount : Int, prefContainers : Array) : IVideoSource? { 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..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 @@ -5,69 +5,121 @@ 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(4); - 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 - throw NotImplementedError("Implement other types"); + fun getTabPosition(tab: ChannelTab): Int { + return _tabs.indexOf(tab) + } + + 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(); - else -> throw IllegalStateException("Invalid tab position $position") - }; - - _cache[position]= fragment; - return fragment; + return fragment } } \ No newline at end of file