Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin K 2024-06-10 12:57:28 +02:00
commit deb3bf6cf7
11 changed files with 783 additions and 350 deletions

View file

@ -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. 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 ## 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. You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes.

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

@ -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<IPlatformPlaylist>? = null
private var _pager: IPager<IPlatformPlaylist>? = null
private var _channel: IPlatformChannel? = null
private var _results: ArrayList<IPlatformContent> = arrayListOf()
private var _adapterResults: InsertedViewAdapterWithLoader<ContentPreviewViewHolder>? = null
val onContentClicked = Event2<IPlatformContent, Long>()
val onContentUrlClicked = Event2<String, ContentType>()
val onUrlClicked = Event1<String>()
val onChannelClicked = Event1<PlatformAuthorLink>()
val onAddToClicked = Event1<IPlatformContent>()
val onAddToQueueClicked = Event1<IPlatformContent>()
val onAddToWatchLaterClicked = Event1<IPlatformContent>()
val onLongPress = Event1<IPlatformContent>()
private fun getPlaylistPager(channel: IPlatformChannel): IPager<IPlatformPlaylist> {
Logger.i(TAG, "getPlaylistPager")
return StatePlatform.instance.getChannelPlaylists(channel.url)
}
private val _taskLoadPlaylists =
TaskHandler<IPlatformChannel, IPager<IPlatformPlaylist>>({ lifecycleScope }, {
val livePager = getPlaylistPager(it)
return@TaskHandler livePager
}).success { livePager ->
setLoading(false)
setPager(livePager)
}.exception<ScriptCaptchaRequiredException> { }.exception<Throwable> {
Logger.w(TAG, "Failed to load initial playlists.", it)
UIDialogs.showGeneralRetryErrorDialog(requireContext(),
it.message ?: "",
it,
{ loadNextPage() })
}
private var _nextPageHandler: TaskHandler<IPager<IPlatformPlaylist>, List<IPlatformPlaylist>> =
TaskHandler<IPager<IPlatformPlaylist>, List<IPlatformPlaylist>>({ 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<Throwable> {
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<IPlatformPlaylist>
) {
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<IPlatformPlaylist>?
if (pager is IRefreshPager<*>) {
_pagerParent = pager
pagerToSet = pager.getCurrentPager() as IPager<IPlatformPlaylist>
pager.onPagerChanged.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
try {
loadPagerInternal(it as IPager<IPlatformPlaylist>)
} 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<IPlatformPlaylist>
) {
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<IPlatformPlaylist> = _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 { }
}
}

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,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.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.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
@ -55,459 +62,530 @@ 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>
)
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(tab: ChannelTab) {
_view?.selectTab(selectedTabIndex); _view?.selectTab(tab)
} }
@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"
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 selectTab(tab: ChannelTab) {
(_viewPager.adapter as ChannelViewPagerAdapter).getTabPosition(tab)
} }
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) { private 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)
_fragment.topBar?.onShown(channel); _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
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 { (_viewPager.adapter as ChannelViewPagerAdapter).insert(
it.getFragment<ChannelContentsFragment>().setChannel(channel); playlistPosition,
it.getFragment<ChannelAboutFragment>().setChannel(channel); ChannelTab.PLAYLISTS
it.getFragment<ChannelListFragment>().setChannel(channel); )
it.getFragment<ChannelMonetizationFragment>().setChannel(channel); }
//TODO: Call on other tabs as needed 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) { 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);
//TODO: Call on other tabs as needed
} }
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 { companion object {
val TAG = "ChannelFragment"; const val TAG = "ChannelFragment"
fun newInstance() = ChannelFragment().apply { } fun newInstance() = ChannelFragment().apply { }
} }
} }

View file

@ -41,6 +41,7 @@ import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber 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.adapters.feedtypes.PreviewPostView
import com.futo.platformplayer.views.comments.AddCommentView import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
@ -264,7 +265,7 @@ class PostDetailFragment : MainFragment {
_buttonSupport.setOnClickListener { _buttonSupport.setOnClickListener {
val author = _post?.author ?: _postOverview?.author; val author = _post?.author ?: _postOverview?.author;
author?.let { _fragment.navigate<ChannelFragment>(it).selectTab(2); }; author?.let { _fragment.navigate<ChannelFragment>(it).selectTab(ChannelTab.SUPPORT); };
}; };
_buttonStore.setOnClickListener { _buttonStore.setOnClickListener {

View file

@ -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.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource 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.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.IHLSManifestAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource 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: 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<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers); fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? { fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {

View file

@ -5,69 +5,121 @@ 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(4);
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 getTabPosition(tab: ChannelTab): Int {
if(T::class == ChannelContentsFragment::class) return _tabs.indexOf(tab)
return createFragment(0) as T; }
else if(T::class == ChannelListFragment::class)
return createFragment(1) as T; fun getTabNames(tab: TabLayout.Tab, position: Int) {
//else if(T::class == ChannelStoreFragment::class) tab.text = _tabs[position].name
// return createFragment(2) as T; }
else if(T::class == ChannelMonetizationFragment::class)
return createFragment(2) as T; fun insert(position: Int, tab: ChannelTab) {
else if(T::class == ChannelAboutFragment::class) _supportedFragments[tab.ordinal] = tab
return createFragment(3) as T; _tabs.add(position, tab)
else notifyItemInserted(position)
throw NotImplementedError("Implement other types"); }
fun remove(position: Int) {
_supportedFragments.remove(_tabs[position].ordinal)
_tabs.removeAt(position)
notifyItemRemoved(position)
} }
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();
else -> throw IllegalStateException("Invalid tab position $position")
};
_cache[position]= fragment;
return fragment;
} }
} }