removed hardcoding. fixed bugs. hide CHANNELS and SUPPORT for non polycentric linked channels

This commit is contained in:
Kai DeLorenzo 2024-06-04 20:22:42 -05:00
commit 5edd389e84
No known key found for this signature in database
8 changed files with 505 additions and 481 deletions

View file

@ -114,7 +114,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
} }
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_lastPolycentricProfile = polycentricProfile; _lastPolycentricProfile = polycentricProfile;
if (polycentricProfile == null) { if (polycentricProfile == null) {

View file

@ -309,7 +309,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
_adapterResults?.setLoading(loading); _adapterResults?.setLoading(loading);
} }
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
val p = _lastPolycentricProfile; val p = _lastPolycentricProfile;
if (p != null && polycentricProfile != null && p.system == polycentricProfile.system) { if (p != null && polycentricProfile != null && p.system == polycentricProfile.system) {
Logger.i(TAG, "setPolycentricProfile skipped because previous was same"); Logger.i(TAG, "setPolycentricProfile skipped because previous was same");

View file

@ -124,7 +124,7 @@ class ChannelListFragment : Fragment, IChannelTabFragment {
} }
} }
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_taskLoadChannel.cancel(); _taskLoadChannel.cancel();
_lastPolycentricProfile = polycentricProfile; _lastPolycentricProfile = polycentricProfile;

View file

@ -46,7 +46,7 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
_lastChannel = channel; _lastChannel = channel;
} }
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) { override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_lastPolycentricProfile = polycentricProfile _lastPolycentricProfile = polycentricProfile
if (polycentricProfile != null) { if (polycentricProfile != null) {
_supportView?.setPolycentricProfile(polycentricProfile) _supportView?.setPolycentricProfile(polycentricProfile)

View file

@ -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.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.contents.ContentType 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.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.platforms.js.models.JSPager
import com.futo.platformplayer.api.media.structures.IAsyncPager import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager 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.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.exceptions.ChannelException 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.logging.Logger
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
@ -42,15 +39,13 @@ import kotlinx.coroutines.launch
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment { class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
private var _recyclerResults: RecyclerView? = null private var _recyclerResults: RecyclerView? = null
private var _llmVideo: LinearLayoutManager? = null private var _llmPlaylist: LinearLayoutManager? = null
private var _loading = false private var _loading = false
private var _pagerParent: IPager<IPlatformContent>? = null private var _pagerParent: IPager<IPlatformPlaylist>? = null
private var _pager: IPager<IPlatformContent>? = null private var _pager: IPager<IPlatformPlaylist>? = null
private var _cache: FeedView.ItemCache<IPlatformContent>? = null
private var _channel: IPlatformChannel? = null private var _channel: IPlatformChannel? = null
private var _results: ArrayList<IPlatformContent> = arrayListOf() private var _results: ArrayList<IPlatformContent> = arrayListOf()
private var _adapterResults: InsertedViewAdapterWithLoader<ContentPreviewViewHolder>? = null private var _adapterResults: InsertedViewAdapterWithLoader<ContentPreviewViewHolder>? = null
private var _lastPolycentricProfile: PolycentricProfile? = null
val onContentClicked = Event2<IPlatformContent, Long>() val onContentClicked = Event2<IPlatformContent, Long>()
val onContentUrlClicked = Event2<String, ContentType>() val onContentUrlClicked = Event2<String, ContentType>()
@ -61,62 +56,49 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
val onAddToWatchLaterClicked = Event1<IPlatformContent>() val onAddToWatchLaterClicked = Event1<IPlatformContent>()
val onLongPress = Event1<IPlatformContent>() val onLongPress = Event1<IPlatformContent>()
private fun getPlaylistPager(channel: IPlatformChannel): IPager<IPlatformContent> { private fun getPlaylistPager(channel: IPlatformChannel): IPager<IPlatformPlaylist> {
Logger.i(TAG, "getPlaylistPager") Logger.i(TAG, "getPlaylistPager")
return StatePlatform.instance.getChannelPlaylists(channel.url) as IPager<IPlatformContent> return StatePlatform.instance.getChannelPlaylists(channel.url)
} }
private val _taskLoadVideos = private val _taskLoadPlaylists =
TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({ lifecycleScope }, { TaskHandler<IPlatformChannel, IPager<IPlatformPlaylist>>({ lifecycleScope }, {
val livePager = getPlaylistPager(it) val livePager = getPlaylistPager(it)
return@TaskHandler if (_channel?.let { channel -> return@TaskHandler livePager
StateSubscriptions.instance.isSubscribed(
channel
)
} == true)
StateCache.cachePagerResults(lifecycleScope, livePager)
else livePager
}).success { livePager -> }).success { livePager ->
setLoading(false) setLoading(false)
setPager(livePager) setPager(livePager)
} }.exception<ScriptCaptchaRequiredException> { }.exception<Throwable> {
.exception<ScriptCaptchaRequiredException> { } Logger.w(TAG, "Failed to load initial playlists.", it)
.exception<Throwable> { UIDialogs.showGeneralRetryErrorDialog(requireContext(),
Logger.w(TAG, "Failed to load initial videos.", it)
UIDialogs.showGeneralRetryErrorDialog(
requireContext(),
it.message ?: "", it.message ?: "",
it, it,
{ loadNextPage() }) { loadNextPage() })
} }
private var _nextPageHandler: TaskHandler<IPager<IPlatformContent>, List<IPlatformContent>> = private var _nextPageHandler: TaskHandler<IPager<IPlatformPlaylist>, List<IPlatformPlaylist>> =
TaskHandler<IPager<IPlatformContent>, List<IPlatformContent>>({ lifecycleScope }, { TaskHandler<IPager<IPlatformPlaylist>, List<IPlatformPlaylist>>({ lifecycleScope }, {
if (it is IAsyncPager<*>) if (it is IAsyncPager<*>) it.nextPageAsync()
it.nextPageAsync() else it.nextPage()
else
it.nextPage()
processPagerExceptions(it) processPagerExceptions(it)
return@TaskHandler it.getResults() return@TaskHandler it.getResults()
}).success { }).success {
setLoading(false) setLoading(false)
val posBefore = _results.size val posBefore = _results.size
//val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
_results.addAll(it) _results.addAll(it)
_adapterResults?.let { adapterVideo -> _adapterResults?.let { adapterResult ->
adapterVideo.notifyItemRangeInserted( adapterResult.notifyItemRangeInserted(
adapterVideo.childToParentPosition( adapterResult.childToParentPosition(
posBefore posBefore
), it.size ), it.size
) )
} }
}.exception<Throwable> { }.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it) Logger.w(TAG, "Failed to load next page.", it)
UIDialogs.showGeneralRetryErrorDialog( UIDialogs.showGeneralRetryErrorDialog(requireContext(),
requireContext(),
it.message ?: "", it.message ?: "",
it, it,
{ loadNextPage() }) { loadNextPage() })
@ -127,10 +109,10 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
val recyclerResults = _recyclerResults ?: return val recyclerResults = _recyclerResults ?: return
val llmVideo = _llmVideo ?: return val llmPlaylist = _llmPlaylist ?: return
val visibleItemCount = recyclerResults.childCount val visibleItemCount = recyclerResults.childCount
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition() val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
val visibleThreshold = 15 val visibleThreshold = 15
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size) { if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size) {
loadNextPage() loadNextPage()
@ -147,7 +129,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
Logger.i(TAG, "setChannel setChannel=${channel}") Logger.i(TAG, "setChannel setChannel=${channel}")
_taskLoadVideos.cancel() _taskLoadPlaylists.cancel()
_channel = channel _channel = channel
_results.clear() _results.clear()
@ -157,20 +139,14 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
container: ViewGroup?,
savedInstanceState: Bundle?
): View? { ): View? {
val view = inflater.inflate(R.layout.fragment_channel_videos, container, false) val view = inflater.inflate(R.layout.fragment_channel_videos, container, false)
_recyclerResults = view.findViewById(R.id.recycler_videos) _recyclerResults = view.findViewById(R.id.recycler_videos)
_adapterResults = PreviewContentListAdapter( _adapterResults = PreviewContentListAdapter(
view.context, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
FeedStyle.THUMBNAIL,
_results,
null,
Settings.instance.channel.progressBar
).apply { ).apply {
this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit) this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit)
this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit) this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit)
@ -182,9 +158,9 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit) this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
} }
_llmVideo = LinearLayoutManager(view.context) _llmPlaylist = LinearLayoutManager(view.context)
_recyclerResults?.adapter = _adapterResults _recyclerResults?.adapter = _adapterResults
_recyclerResults?.layoutManager = _llmVideo _recyclerResults?.layoutManager = _llmPlaylist
_recyclerResults?.addOnScrollListener(_scrollListener) _recyclerResults?.addOnScrollListener(_scrollListener)
return view return view
@ -196,31 +172,29 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
_recyclerResults = null _recyclerResults = null
_pager = null _pager = null
_taskLoadVideos.cancel() _taskLoadPlaylists.cancel()
_nextPageHandler.cancel() _nextPageHandler.cancel()
} }
private fun setPager( private fun setPager(
pager: IPager<IPlatformContent>, pager: IPager<IPlatformPlaylist>
cache: FeedView.ItemCache<IPlatformContent>? = null
) { ) {
if (_pagerParent != null && _pagerParent is IRefreshPager<*>) { if (_pagerParent != null && _pagerParent is IRefreshPager<*>) {
(_pagerParent as IRefreshPager<*>).onPagerError.remove(this) (_pagerParent as IRefreshPager<*>).onPagerError.remove(this)
(_pagerParent as IRefreshPager<*>).onPagerChanged.remove(this) (_pagerParent as IRefreshPager<*>).onPagerChanged.remove(this)
_pagerParent = null _pagerParent = null
} }
if (_pager is IReplacerPager<*>) if (_pager is IReplacerPager<*>) (_pager as IReplacerPager<*>).onReplaced.remove(this)
(_pager as IReplacerPager<*>).onReplaced.remove(this)
val pagerToSet: IPager<IPlatformContent>? val pagerToSet: IPager<IPlatformPlaylist>?
if (pager is IRefreshPager<*>) { if (pager is IRefreshPager<*>) {
_pagerParent = pager _pagerParent = pager
pagerToSet = pager.getCurrentPager() as IPager<IPlatformContent> pagerToSet = pager.getCurrentPager() as IPager<IPlatformPlaylist>
pager.onPagerChanged.subscribe(this) { pager.onPagerChanged.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
try { try {
loadPagerInternal(it as IPager<IPlatformContent>) loadPagerInternal(it as IPager<IPlatformPlaylist>)
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "loadPagerInternal failed.", e) Logger.e(TAG, "loadPagerInternal failed.", e)
} }
@ -228,33 +202,26 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
} }
pager.onPagerError.subscribe(this) { pager.onPagerError.subscribe(this) {
Logger.e(TAG, "Search pager failed: ${it.message}", it) Logger.e(TAG, "Search pager failed: ${it.message}", it)
if (it is PluginException) if (it is PluginException) UIDialogs.toast("Plugin [${it.config.name}] failed due to:\n${it.message}")
UIDialogs.toast("Plugin [${it.config.name}] failed due to:\n${it.message}") else UIDialogs.toast("Plugin failed due to:\n${it.message}")
else
UIDialogs.toast("Plugin failed due to:\n${it.message}")
} }
} else pagerToSet = pager } else pagerToSet = pager
loadPagerInternal(pagerToSet, cache) loadPagerInternal(pagerToSet)
} }
private fun loadPagerInternal( private fun loadPagerInternal(
pager: IPager<IPlatformContent>, pager: IPager<IPlatformPlaylist>
cache: FeedView.ItemCache<IPlatformContent>? = null
) { ) {
_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<*>) { if (pager is IReplacerPager<*>) {
pager.onReplaced.subscribe(this) { oldItem, newItem -> pager.onReplaced.subscribe(this) { oldItem, newItem ->
if (_pager != pager) if (_pager != pager) return@subscribe
return@subscribe
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
val toReplaceIndex = _results.indexOfFirst { it == oldItem } val toReplaceIndex = _results.indexOfFirst { it == oldItem }
if (toReplaceIndex >= 0) { if (toReplaceIndex >= 0) {
_results[toReplaceIndex] = newItem as IPlatformContent _results[toReplaceIndex] = newItem as IPlatformPlaylist
_adapterResults?.let { _adapterResults?.let {
it.notifyItemChanged(it.childToParentPosition(toReplaceIndex)) it.notifyItemChanged(it.childToParentPosition(toReplaceIndex))
} }
@ -277,11 +244,11 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
private fun loadInitial() { private fun loadInitial() {
val channel: IPlatformChannel = _channel ?: return val channel: IPlatformChannel = _channel ?: return
setLoading(true) setLoading(true)
_taskLoadVideos.run(channel) _taskLoadPlaylists.run(channel)
} }
private fun loadNextPage() { private fun loadNextPage() {
val pager: IPager<IPlatformContent> = _pager ?: return val pager: IPager<IPlatformPlaylist> = _pager ?: return
if (_pager?.hasMorePages() == true) { if (_pager?.hasMorePages() == true) {
setLoading(true) setLoading(true)
_nextPageHandler.run(pager) _nextPageHandler.run(pager)
@ -293,32 +260,11 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
_adapterResults?.setLoading(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<*>) { private fun processPagerExceptions(pager: IPager<*>) {
if (pager is MultiPager<*> && pager.allowFailure) { if (pager is MultiPager<*> && pager.allowFailure) {
val ex = pager.getResultExceptions() val ex = pager.getResultExceptions()
for (kv in ex) { 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 MultiPager<*> -> (kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>?
is JSPager<*> -> kv.key as JSPager<*> is JSPager<*> -> kv.key as JSPager<*>
else -> null else -> null
@ -329,14 +275,12 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
try { try {
val channel = val channel =
if (kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null if (kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null
if (jsVideoPager != null) if (jsPager != null) UIDialogs.toast(
UIDialogs.toast( it,
it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n" + "Plugin ${jsPager.getPluginConfig().name} failed:\n" + (if (!channel.isNullOrEmpty()) "(${channel}) " else "") + "${kv.value.message}",
(if (!channel.isNullOrEmpty()) "(${channel}) " else "") + false
"${kv.value.message}", false )
) else UIDialogs.toast(it, kv.value.message ?: "", false)
else
UIDialogs.toast(it, kv.value.message ?: "", false)
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e) Logger.e(TAG, "Failed to show toast.", e)
} }

View file

@ -1,7 +1,11 @@
package com.futo.platformplayer.fragment.channel.tab package com.futo.platformplayer.fragment.channel.tab
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
interface IChannelTabFragment { interface IChannelTabFragment {
fun setChannel(channel: IPlatformChannel); fun setChannel(channel: IPlatformChannel)
} fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
}
}

View file

@ -15,8 +15,9 @@ import androidx.appcompat.widget.AppCompatImageView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.*
import com.futo.platformplayer.R 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.PlatformID
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.api.media.models.PlatformAuthorLink 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.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.assume
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
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.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache 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.StatePlatform
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptions 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.adapters.ChannelViewPagerAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.subscriptions.SubscribeButton 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.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -56,476 +62,510 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class PolycentricProfile(val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>); data class PolycentricProfile(
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
const val PLAYLIST_POSITION = 4 )
class ChannelFragment : MainFragment() { class ChannelFragment : MainFragment() {
override val isMainView : Boolean = true; override val isMainView: Boolean = true
override val hasBottomBar: Boolean = true; override val hasBottomBar: Boolean = true
private var _view: ChannelView? = null; private var _view: ChannelView? = null
override fun onShownWithView(parameter: Any?, isBack: Boolean) { override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack); super.onShownWithView(parameter, isBack)
_view?.onShown(parameter, isBack); _view?.onShown(parameter, isBack)
} }
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateMainView(
val view = ChannelView(this, inflater); inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
_view = view; ): View {
return view; val view = ChannelView(this, inflater)
_view = view
return view
} }
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
return _view?.onBackPressed() ?: false; return _view?.onBackPressed() ?: false
} }
override fun onDestroyMainView() { override fun onDestroyMainView() {
super.onDestroyMainView(); super.onDestroyMainView()
_view?.cleanup(); _view?.cleanup()
_view = null; _view = null
} }
fun selectTab(selectedTabIndex: Int) { fun selectTab(selectedTabIndex: Int) {
_view?.selectTab(selectedTabIndex); _view?.selectTab(selectedTabIndex)
} }
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class ChannelView : LinearLayout { class ChannelView
private val _fragment: ChannelFragment; (fragment: ChannelFragment, inflater: LayoutInflater) : LinearLayout(inflater.context) {
private val _fragment: ChannelFragment = fragment
private var _textChannel: TextView; private var _textChannel: TextView
private var _textChannelSub: TextView; private var _textChannelSub: TextView
private var _creatorThumbnail: CreatorThumbnail; private var _creatorThumbnail: CreatorThumbnail
private var _imageBanner: AppCompatImageView; private var _imageBanner: AppCompatImageView
private var _tabs: TabLayout; private var _tabs: TabLayout
private var _viewPager: ViewPager2; private var _viewPager: ViewPager2
private var _tabLayoutMediator: TabLayoutMediator;
private var _buttonSubscribe: SubscribeButton;
private var _buttonSubscriptionSettings: ImageButton;
private var _overlayContainer: FrameLayout; // private var _adapter: ChannelViewPagerAdapter;
private var _overlay_loading: LinearLayout; private var _tabLayoutMediator: TabLayoutMediator
private var _overlay_loading_spinner: ImageView; 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 _slideUpOverlay: SlideUpMenuOverlay? = null
private var _selectedTabIndex: Int = -1;
private var _isLoading: Boolean = false
private var _selectedTabIndex: Int = -1
var channel: IPlatformChannel? = null var channel: IPlatformChannel? = null
private set; private set
private var _url: String? = null; private var _url: String? = null
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() { 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 _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>; private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>; private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
constructor(fragment: ChannelFragment, inflater: LayoutInflater) : super(inflater.context) { init {
_fragment = fragment; inflater.inflate(R.layout.fragment_channel, this)
inflater.inflate(R.layout.fragment_channel, this); _taskLoadPolycentricProfile =
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
_taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({fragment.lifecycleScope}, { id -> { id ->
return@TaskHandler PolycentricCache.instance.getProfileAsync(id); return@TaskHandler PolycentricCache.instance.getProfileAsync(id)
}) }).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
.success { it -> setPolycentricProfile(it, animate = true) } Logger.w(TAG, "Failed to load polycentric profile.", it)
.exception<Throwable> { }
Logger.w(TAG, "Failed to load polycentric profile.", it); _taskGetChannel = TaskHandler<String, IPlatformChannel>({ fragment.lifecycleScope },
}; { url -> StatePlatform.instance.getChannelLive(url) }).success { showChannel(it); }
_taskGetChannel = TaskHandler<String, IPlatformChannel>({fragment.lifecycleScope}, { url -> StatePlatform.instance.getChannelLive(url) })
.success { showChannel(it); }
.exception<NoPlatformClientException> { .exception<NoPlatformClientException> {
UIDialogs.showDialog(context, UIDialogs.showDialog(
context,
R.drawable.ic_sources, 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, 0,
UIDialogs.Action("Back", { UIDialogs.Action("Back", {
fragment.close(true); fragment.close(true)
}, UIDialogs.ActionStyle.PRIMARY) }, UIDialogs.ActionStyle.PRIMARY)
); )
}.exception<Throwable> {
Logger.e(TAG, "Failed to load channel.", it)
UIDialogs.showGeneralRetryErrorDialog(
context, it.message ?: "", it, { loadChannel() }, null, fragment
)
} }
.exception<Throwable> { val tabs: TabLayout = findViewById(R.id.tabs)
Logger.e(TAG, "Failed to load channel.", it); val viewPager: ViewPager2 = findViewById(R.id.view_pager)
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() }, null, fragment); _textChannel = findViewById(R.id.text_channel_name)
} _textChannelSub = findViewById(R.id.text_metadata)
_creatorThumbnail = findViewById(R.id.creator_thumbnail)
val tabs: TabLayout = findViewById(R.id.tabs); _imageBanner = findViewById(R.id.image_channel_banner)
val viewPager: ViewPager2 = findViewById(R.id.view_pager); _buttonSubscribe = findViewById(R.id.button_subscribe)
_textChannel = findViewById(R.id.text_channel_name); _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
_textChannelSub = findViewById(R.id.text_metadata); _overlayLoading = findViewById(R.id.channel_loading_overlay)
_creatorThumbnail = findViewById(R.id.creator_thumbnail); _overlayLoadingSpinner = findViewById(R.id.channel_loader)
_imageBanner = findViewById(R.id.image_channel_banner); _overlayContainer = findViewById(R.id.overlay_container)
_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);
_buttonSubscribe.onSubscribed.subscribe { _buttonSubscribe.onSubscribed.subscribe {
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer); UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; _buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
} }
_buttonSubscribe.onUnSubscribed.subscribe { _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 { _buttonSubscriptionSettings.setOnClickListener {
val url = channel?.url ?: _url ?: return@setOnClickListener; val url = channel?.url ?: _url ?: return@setOnClickListener
val sub = StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener; val sub =
UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer); StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener
}; UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer)
}
//TODO: Determine if this is really the only solution (isSaveEnabled=false) //TODO: Determine if this is really the only solution (isSaveEnabled=false)
viewPager.isSaveEnabled = false; viewPager.isSaveEnabled = false
viewPager.registerOnPageChangeCallback(_onPageChangeCallback); viewPager.registerOnPageChangeCallback(_onPageChangeCallback)
val adapter = ChannelViewPagerAdapter(fragment.childFragmentManager, fragment.lifecycle); val adapter = ChannelViewPagerAdapter(fragment.childFragmentManager, fragment.lifecycle)
adapter.onChannelClicked.subscribe { c -> fragment.navigate<ChannelFragment>(c) } adapter.onChannelClicked.subscribe { c -> fragment.navigate<ChannelFragment>(c) }
adapter.onContentClicked.subscribe { v, _ -> adapter.onContentClicked.subscribe { v, _ ->
if(v is IPlatformVideo) { when (v) {
StatePlayer.instance.clearQueue(); is IPlatformVideo -> {
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail(); StatePlayer.instance.clearQueue()
} else if (v is IPlatformPlaylist) { fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
fragment.navigate<PlaylistFragment>(v); }
} else if (v is IPlatformPost) {
fragment.navigate<PostDetailFragment>(v); is IPlatformPlaylist -> {
fragment.navigate<PlaylistFragment>(v)
}
is IPlatformPost -> {
fragment.navigate<PostDetailFragment>(v)
}
} }
} }
adapter.onAddToClicked.subscribe {content -> adapter.onAddToClicked.subscribe { content ->
_overlayContainer.let { _overlayContainer.let {
if(content is IPlatformVideo) if (content is IPlatformVideo) _slideUpOverlay =
_slideUpOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it); UISlideOverlays.showVideoOptionsOverlay(content, it)
} }
} }
adapter.onAddToQueueClicked.subscribe { content -> adapter.onAddToQueueClicked.subscribe { content ->
if(content is IPlatformVideo) { if (content is IPlatformVideo) {
StatePlayer.instance.addToQueue(content); StatePlayer.instance.addToQueue(content)
} }
} }
adapter.onAddToWatchLaterClicked.subscribe { content -> adapter.onAddToWatchLaterClicked.subscribe { content ->
if(content is IPlatformVideo) { if (content is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content)); StatePlaylists.instance.addToWatchLater(
UIDialogs.toast("Added to watch later\n[${content.name}]"); SerializedPlatformVideo.fromVideo(
content
)
)
UIDialogs.toast("Added to watch later\n[${content.name}]")
} }
} }
adapter.onUrlClicked.subscribe { url -> adapter.onUrlClicked.subscribe { url ->
fragment.navigate<BrowserFragment>(url); fragment.navigate<BrowserFragment>(url)
} }
adapter.onContentUrlClicked.subscribe { url, contentType -> adapter.onContentUrlClicked.subscribe { url, contentType ->
when(contentType) { when (contentType) {
ContentType.MEDIA -> { ContentType.MEDIA -> {
StatePlayer.instance.clearQueue(); StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail(); fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
}; }
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
else -> {}; ContentType.URL -> fragment.navigate<BrowserFragment>(url)
else -> {}
} }
} }
adapter.onLongPress.subscribe { content -> adapter.onLongPress.subscribe { content ->
_overlayContainer.let { _overlayContainer.let {
if(content is IPlatformVideo) if (content is IPlatformVideo) _slideUpOverlay =
_slideUpOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it); UISlideOverlays.showVideoOptionsOverlay(content, it)
} }
} }
viewPager.adapter = adapter; viewPager.adapter = adapter
val tabLayoutMediator = TabLayoutMediator(
val tabLayoutMediator = TabLayoutMediator(tabs, viewPager) { tab, position -> tabs, viewPager, (viewPager.adapter as ChannelViewPagerAdapter)::getTabNames
tab.text = when (position) { )
0 -> "VIDEOS" tabLayoutMediator.attach()
1 -> "CHANNELS"
//2 -> "STORE"
2 -> "SUPPORT"
3 -> "ABOUT"
PLAYLIST_POSITION -> "PLAYLISTS"
else -> "Unknown $position"
};
};
tabLayoutMediator.attach();
_tabLayoutMediator = tabLayoutMediator;
_tabs = tabs;
_viewPager = viewPager;
_tabLayoutMediator = tabLayoutMediator
_tabs = tabs
_viewPager = viewPager
if (_selectedTabIndex != -1) { if (_selectedTabIndex != -1) {
selectTab(_selectedTabIndex); selectTab(_selectedTabIndex)
} }
setLoading(true)
setLoading(true);
} }
fun cleanup() { fun cleanup() {
_taskLoadPolycentricProfile.cancel(); _taskLoadPolycentricProfile.cancel()
_taskGetChannel.cancel(); _taskGetChannel.cancel()
_tabLayoutMediator.detach(); _tabLayoutMediator.detach()
_viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback); _viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback)
hideSlideUpOverlay(); hideSlideUpOverlay()
(_overlay_loading_spinner.drawable as Animatable?)?.stop(); (_overlayLoadingSpinner.drawable as Animatable?)?.stop()
} }
fun onShown(parameter: Any?, isBack: Boolean) { fun onShown(parameter: Any?, isBack: Boolean) {
hideSlideUpOverlay(); hideSlideUpOverlay()
_taskLoadPolycentricProfile.cancel(); _taskLoadPolycentricProfile.cancel()
_selectedTabIndex = -1; _selectedTabIndex = -1
if (!isBack || _url == null) { if (!isBack || _url == null) {
_imageBanner.setImageDrawable(null); _imageBanner.setImageDrawable(null)
if (parameter is String) { when (parameter) {
_buttonSubscribe.setSubscribeChannel(parameter); is String -> {
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; _buttonSubscribe.setSubscribeChannel(parameter)
setPolycentricProfileOr(parameter) { _buttonSubscriptionSettings.visibility =
_textChannel.text = ""; if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
_textChannelSub.text = ""; setPolycentricProfileOr(parameter) {
_creatorThumbnail.setThumbnail(null, true); _textChannel.text = ""
Glide.with(_imageBanner) _textChannelSub.text = ""
.clear(_imageBanner); _creatorThumbnail.setThumbnail(null, true)
}; Glide.with(_imageBanner).clear(_imageBanner)
}
_url = parameter; _url = parameter
loadChannel(); 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);
loadPolycentricProfile(parameter.id, parameter.url) is SerializedChannel -> {
}; showChannel(parameter)
_url = parameter.url
loadChannel()
}
_url = parameter.url; is IPlatformChannel -> showChannel(parameter)
loadChannel(); is PlatformAuthorLink -> {
} else if (parameter is Subscription) { setPolycentricProfileOr(parameter.url) {
setPolycentricProfileOr(parameter.channel.url) { _textChannel.text = parameter.name
_textChannel.text = parameter.channel.name; _textChannelSub.text = ""
_textChannelSub.text = ""; _creatorThumbnail.setThumbnail(parameter.thumbnail, true)
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true); Glide.with(_imageBanner).clear(_imageBanner)
Glide.with(_imageBanner)
.clear(_imageBanner);
loadPolycentricProfile(parameter.channel.id, parameter.channel.url) loadPolycentricProfile(parameter.id, parameter.url)
}; }
_url = parameter.channel.url; _url = parameter.url
loadChannel(); 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 { } else {
loadChannel(); loadChannel()
} }
} }
fun selectTab(selectedTabIndex: Int) { fun selectTab(selectedTabIndex: Int) {
_selectedTabIndex = selectedTabIndex; _selectedTabIndex = selectedTabIndex
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex)); _tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
} }
private fun loadPolycentricProfile(id: PlatformID, url: String) { private fun loadPolycentricProfile(id: PlatformID, url: String) {
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true); val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
if (cachedPolycentricProfile != null) { if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = true) setPolycentricProfile(cachedPolycentricProfile, animate = true)
if (cachedPolycentricProfile.expired) { if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(id); _taskLoadPolycentricProfile.run(id)
} }
} else { } else {
_taskLoadPolycentricProfile.run(id); _taskLoadPolycentricProfile.run(id)
} }
} }
private fun setLoading(isLoading: Boolean) { private fun setLoading(isLoading: Boolean) {
if (_isLoading == isLoading) { if (_isLoading == isLoading) {
return; return
} }
_isLoading = isLoading; _isLoading = isLoading
if(isLoading){ if (isLoading) {
_overlay_loading.visibility = View.VISIBLE; _overlayLoading.visibility = View.VISIBLE
(_overlay_loading_spinner.drawable as Animatable?)?.start(); (_overlayLoadingSpinner.drawable as Animatable?)?.start()
} } else {
else { (_overlayLoadingSpinner.drawable as Animatable?)?.stop()
(_overlay_loading_spinner.drawable as Animatable?)?.stop(); _overlayLoading.visibility = View.GONE
_overlay_loading.visibility = View.GONE;
} }
} }
fun onBackPressed(): Boolean { fun onBackPressed(): Boolean {
if (_slideUpOverlay != null) { if (_slideUpOverlay != null) {
hideSlideUpOverlay(); hideSlideUpOverlay()
return true; return true
} }
return false; return false
} }
private fun hideSlideUpOverlay() { private fun hideSlideUpOverlay() {
_slideUpOverlay?.hide(false); _slideUpOverlay?.hide(false)
_slideUpOverlay = null; _slideUpOverlay = null
} }
private fun loadChannel() { private fun loadChannel() {
val url = _url; val url = _url
if (url != null) { if (url != null) {
setLoading(true); setLoading(true)
_taskGetChannel.run(url); _taskGetChannel.run(url)
} }
} }
private fun showChannel(channel: IPlatformChannel) { private fun showChannel(channel: IPlatformChannel) {
setLoading(false); setLoading(false)
if (!StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) { _fragment.topBar?.onShown(channel)
_tabs.removeTabAt(PLAYLIST_POSITION)
}
_fragment.topBar?.onShown(channel);
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) { 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.showConfirmationDialog(context,
UIDialogs.showDialogProgress(context) { context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist)
_fragment.lifecycleScope.launch(Dispatchers.IO) { .replace("{channelName}", channel.name),
try { {
StatePlaylists.instance.createPlaylistFromChannel(channel) { page -> UIDialogs.showDialogProgress(context) {
_fragment.lifecycleScope.launch(Dispatchers.Main) { _fragment.lifecycleScope.launch(Dispatchers.IO) {
it.setText("${channel.name}\n" + context.getString(R.string.page) + " $page"); 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)
catch(ex: Exception) { UIDialogs.showGeneralErrorDialog(
Logger.e(TAG, "Error", ex); context,
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_convert_channel), ex); context.getString(R.string.failed_to_convert_channel),
} ex
)
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
it.hide(); it.hide()
}
} }
}; }
}; })
}); })
});
_fragment.lifecycleScope.launch(Dispatchers.IO) { _fragment.lifecycleScope.launch(Dispatchers.IO) {
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url); val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (plugin != null && plugin.capabilities.hasSearchChannelContents) { if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
buttons.add(Pair(R.drawable.ic_search) { buttons.add(Pair(R.drawable.ic_search) {
_fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO, channel.url)); _fragment.navigate<SuggestionsFragment>(
}); SuggestionsFragmentData(
"", SearchType.VIDEO, channel.url
)
)
})
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons); _fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
} }
} }
} }
_buttonSubscribe.setSubscribeChannel(channel); _buttonSubscribe.setSubscribeChannel(channel)
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE; _buttonSubscriptionSettings.visibility =
_textChannel.text = channel.name; if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else ""; _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
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let { if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
it.getFragment<ChannelContentsFragment>().setChannel(channel); ChannelTab.PLAYLISTS.ordinal.toLong()
it.getFragment<ChannelAboutFragment>().setChannel(channel); )
it.getFragment<ChannelListFragment>().setChannel(channel); ) {
it.getFragment<ChannelMonetizationFragment>().setChannel(channel); (_viewPager.adapter as ChannelViewPagerAdapter).insert(2, ChannelTab.PLAYLISTS)
if (StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) { }
Logger.w(TAG, "Supported channel playlists??"); if (!supportsPlaylists && (_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
it.getFragment<ChannelPlaylistsFragment>().setChannel(channel); ChannelTab.PLAYLISTS.ordinal.toLong()
} )
//TODO: Call on other tabs as needed ) {
(_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) { setPolycentricProfileOr(channel.url) {
_textChannel.text = channel.name; _textChannel.text = channel.name
_creatorThumbnail.setThumbnail(channel.thumbnail, true); _creatorThumbnail.setThumbnail(channel.thumbnail, true)
Glide.with(_imageBanner) Glide.with(_imageBanner).load(channel.banner).crossfade().into(_imageBanner)
.load(channel.banner)
.crossfade()
.into(_imageBanner);
_taskLoadPolycentricProfile.run(channel.id); _taskLoadPolycentricProfile.run(channel.id)
}; }
} }
private fun setPolycentricProfileOr(url: String, or: () -> Unit) { 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) { if (cachedProfile != null) {
setPolycentricProfile(cachedProfile, animate = false); setPolycentricProfile(cachedProfile, animate = false)
} else { } else {
or(); or()
} }
} }
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { private fun setPolycentricProfile(
val dp_35 = 35.dp(resources) cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
val profile = cachedPolycentricProfile?.profile; ) {
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35) val dp35 = 35.dp(resources)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; val profile = cachedPolycentricProfile?.profile
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
if (avatar != null) { it.toURLInfoSystemLinkUrl(
_creatorThumbnail.setThumbnail(avatar, animate); profile.system.toProto(), it.process, profile.systemState.servers.toList()
} else { )
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
} }
val banner = profile?.systemState?.banner?.selectHighestResolutionImage() if (avatar != null) {
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; _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) { if (banner != null) {
Glide.with(_imageBanner) Glide.with(_imageBanner).load(banner).crossfade().into(_imageBanner)
.load(banner)
.crossfade()
.into(_imageBanner);
} else { } else {
Glide.with(_imageBanner) Glide.with(_imageBanner).load(channel?.banner).crossfade().into(_imageBanner)
.load(channel?.banner)
.crossfade()
.into(_imageBanner);
} }
if (profile != null) { if (profile != null) {
_fragment.topBar?.onShown(profile); _fragment.topBar?.onShown(profile)
_textChannel.text = profile.systemState.username; _textChannel.text = profile.systemState.username
} }
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let { // sets the profile for each tab
it.getFragment<ChannelAboutFragment>().setPolycentricProfile(profile); for (fragment in _fragment.childFragmentManager.fragments) {
it.getFragment<ChannelMonetizationFragment>().setPolycentricProfile(profile); (fragment as IChannelTabFragment).setPolycentricProfile(profile)
it.getFragment<ChannelListFragment>().setPolycentricProfile(profile);
it.getFragment<ChannelContentsFragment>().setPolycentricProfile(profile);
channel?.let { channel ->
if (StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists) {
Logger.w(TAG, "Supported channel playlists??");
it.getFragment<ChannelPlaylistsFragment>().setPolycentricProfile(profile);
}
}
//TODO: Call on other tabs as needed
} }
//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 { companion object {
val TAG = "ChannelFragment"; const val TAG = "ChannelFragment"
fun newInstance() = ChannelFragment().apply { } fun newInstance() = ChannelFragment().apply { }
} }
} }

View file

@ -5,81 +5,117 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.futo.platformplayer.api.media.models.PlatformAuthorLink 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.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2 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<Fragment?> = arrayOfNulls(5);
val onContentUrlClicked = Event2<String, ContentType>(); enum class ChannelTab {
val onUrlClicked = Event1<String>(); VIDEOS, CHANNELS, PLAYLISTS, SUPPORT, ABOUT
val onContentClicked = Event2<IPlatformContent, Long>(); }
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformContent>(); class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
val onAddToQueueClicked = Event1<IPlatformContent>(); FragmentStateAdapter(fragmentManager, lifecycle) {
val onAddToWatchLaterClicked = Event1<IPlatformContent>(); private val _supportedFragments = mutableMapOf(
val onLongPress = Event1<IPlatformContent>(); 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<String, ContentType>()
val onUrlClicked = Event1<String>()
val onContentClicked = Event2<IPlatformContent, Long>()
val onChannelClicked = Event1<PlatformAuthorLink>()
val onAddToClicked = Event1<IPlatformContent>()
val onAddToQueueClicked = Event1<IPlatformContent>()
val onAddToWatchLaterClicked = Event1<IPlatformContent>()
val onLongPress = Event1<IPlatformContent>()
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 { override fun getItemCount(): Int {
return _cache.size; return _supportedFragments.size
} }
inline fun <reified T:IChannelTabFragment> getFragment(): T {
//TODO: I have a feeling this can somehow be synced with createFragment so only 1 mapping exists (without a Map<>) fun getTabNames(tab: TabLayout.Tab, position: Int) {
if(T::class == ChannelContentsFragment::class) tab.text = _tabs[position].name
return createFragment(0) as T; }
else if(T::class == ChannelListFragment::class)
return createFragment(1) as T; fun insert(position: Int, tab: ChannelTab) {
//else if(T::class == ChannelStoreFragment::class) _supportedFragments[tab.ordinal] = tab
// return createFragment(2) as T; _tabs.add(position, tab)
else if(T::class == ChannelMonetizationFragment::class) notifyItemInserted(position)
return createFragment(2) as T; }
else if(T::class == ChannelAboutFragment::class)
return createFragment(3) as T; fun remove(position: Int) {
else if(T::class == ChannelPlaylistsFragment::class) _supportedFragments.remove(_tabs[position].ordinal)
return createFragment(4) as T; _tabs.removeAt(position)
else notifyItemRemoved(position)
throw NotImplementedError("Implement other types");
} }
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
val cachedFragment = _cache[position]; val fragment: Fragment
if (cachedFragment != null) { when (_tabs[position]) {
return cachedFragment; 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) { return fragment
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;
} }
} }