From 916936e1792289980e15bd8e41f8028636180f6a Mon Sep 17 00:00:00 2001 From: Koen Date: Fri, 14 Jun 2024 13:32:00 +0200 Subject: [PATCH] Implemented proper remote playlist support. --- .../platformplayer/activities/MainActivity.kt | 4 + .../mainactivity/main/ChannelFragment.kt | 2 +- .../mainactivity/main/ContentFeedView.kt | 14 +- .../main/ContentSearchResultsFragment.kt | 2 +- .../mainactivity/main/PlaylistFragment.kt | 53 --- .../main/RemotePlaylistFragment.kt | 362 ++++++++++++++++++ .../adapters/InsertedViewAdapterWithLoader.kt | 1 + .../adapters/VideoListEditorViewHolder.kt | 4 +- .../res/layout/fragment_remote_playlist.xml | 198 ++++++++++ 9 files changed, 578 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt create mode 100644 app/src/main/res/layout/fragment_remote_playlist.xml diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index f9565916..90902f5e 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -104,6 +104,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { lateinit var _fragMainTutorial: TutorialFragment; lateinit var _fragMainPlaylists: PlaylistsFragment; lateinit var _fragMainPlaylist: PlaylistFragment; + lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment; lateinit var _fragWatchlist: WatchLaterFragment; lateinit var _fragHistory: HistoryFragment; lateinit var _fragSourceDetail: SourceDetailFragment; @@ -246,6 +247,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragMainSources = SourcesFragment.newInstance(); _fragMainPlaylists = PlaylistsFragment.newInstance(); _fragMainPlaylist = PlaylistFragment.newInstance(); + _fragMainRemotePlaylist = RemotePlaylistFragment.newInstance(); _fragPostDetail = PostDetailFragment.newInstance(); _fragWatchlist = WatchLaterFragment.newInstance(); _fragHistory = HistoryFragment.newInstance(); @@ -331,6 +333,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragMainSources.topBar = _fragTopBarAdd; _fragMainPlaylists.topBar = _fragTopBarGeneral; _fragMainPlaylist.topBar = _fragTopBarNavigation; + _fragMainRemotePlaylist.topBar = _fragTopBarNavigation; _fragPostDetail.topBar = _fragTopBarNavigation; _fragWatchlist.topBar = _fragTopBarNavigation; _fragHistory.topBar = _fragTopBarNavigation; @@ -1044,6 +1047,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { SourcesFragment::class -> _fragMainSources as T; PlaylistsFragment::class -> _fragMainPlaylists as T; PlaylistFragment::class -> _fragMainPlaylist as T; + RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T; PostDetailFragment::class -> _fragPostDetail as T; WatchLaterFragment::class -> _fragWatchlist as T; HistoryFragment::class -> _fragHistory as T; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index dc419673..ad4aa84d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -204,7 +204,7 @@ class ChannelFragment : MainFragment() { } is IPlatformPlaylist -> { - fragment.navigate(v) + fragment.navigate(v) } is IPlatformPost -> { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt index cde70f32..fcabcb5b 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt @@ -6,28 +6,32 @@ import android.view.LayoutInflater import android.widget.LinearLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.futo.platformplayer.* +import com.futo.platformplayer.R +import com.futo.platformplayer.Settings +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.UISlideOverlays 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.models.post.IPlatformPost import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo -import com.futo.platformplayer.api.media.structures.* +import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.views.FeedStyle -import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewHolder +import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter import com.futo.platformplayer.views.adapters.feedtypes.PreviewNestedVideoViewHolder import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay +import com.futo.platformplayer.withTimestamp import kotlin.math.floor abstract class ContentFeedView : FeedView, ContentPreviewViewHolder> where TFragment : MainFragment { @@ -183,7 +187,7 @@ abstract class ContentFeedView : FeedView(content).maximizeVideoDetail(); } } else if (content is IPlatformPlaylist) { - fragment.navigate(content); + fragment.navigate(content); } else if (content is IPlatformPost) { fragment.navigate(content); } @@ -194,7 +198,7 @@ abstract class ContentFeedView : FeedView(url).maximizeVideoDetail(); }; - ContentType.PLAYLIST -> fragment.navigate(url); + ContentType.PLAYLIST -> fragment.navigate(url); ContentType.URL -> fragment.navigate(url); else -> {}; } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt index f7a34777..076c0542 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt @@ -156,7 +156,7 @@ class ContentSearchResultsFragment : MainFragment() { onSearch.subscribe(this) { if(it.isHttpUrl()) { if(StatePlatform.instance.hasEnabledPlaylistClient(it)) - navigate(it); + navigate(it); else if(StatePlatform.instance.hasEnabledChannelClient(it)) navigate(it); else diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt index 713668fe..7d8a788b 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt @@ -1,14 +1,11 @@ package com.futo.platformplayer.fragment.mainactivity.main import android.annotation.SuppressLint -import android.graphics.drawable.Animatable import android.os.Bundle -import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.app.ShareCompat -import androidx.core.view.setPadding import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.* import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist @@ -17,7 +14,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.downloads.VideoDownload -import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.states.StateApp @@ -30,7 +26,6 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class PlaylistFragment : MainFragment() { override val isMainView : Boolean = true; @@ -70,7 +65,6 @@ class PlaylistFragment : MainFragment() { private val _fragment: PlaylistFragment; private var _playlist: Playlist? = null; - private var _remotePlaylist: IPlatformPlaylistDetails? = null; private var _editPlaylistNameInput: SlideUpMenuTextInput? = null; private var _editPlaylistOverlay: SlideUpMenuOverlay? = null; private var _url: String? = null; @@ -136,7 +130,6 @@ class PlaylistFragment : MainFragment() { return@TaskHandler StatePlatform.instance.getPlaylist(it); }) .success { - _remotePlaylist = it; setName(it.name); //TODO: Implement support for pagination setVideos(it.toPlaylist().videos, false); @@ -155,7 +148,6 @@ class PlaylistFragment : MainFragment() { if (parameter is Playlist?) { _playlist = parameter; - _remotePlaylist = null; _url = null; if(parameter != null) { @@ -175,7 +167,6 @@ class PlaylistFragment : MainFragment() { //TODO: Do I have to remove the showConvertPlaylistButton(); button here? } else if (parameter is IPlatformPlaylist) { _playlist = null; - _remotePlaylist = null; _url = parameter.url; setVideoCount(parameter.videoCount); @@ -185,10 +176,8 @@ class PlaylistFragment : MainFragment() { setButtonEditVisible(false); fetchPlaylist(); - showConvertPlaylistButton(); } else if (parameter is String) { _playlist = null; - _remotePlaylist = null; _url = parameter; setName(null); @@ -198,7 +187,6 @@ class PlaylistFragment : MainFragment() { setButtonEditVisible(false); fetchPlaylist(); - showConvertPlaylistButton(); } _playlist?.let { @@ -242,34 +230,6 @@ class PlaylistFragment : MainFragment() { StateDownloads.instance.onDownloadedChanged.remove(this); } - private fun showConvertPlaylistButton() { - _fragment.topBar?.assume()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) { - val remotePlaylist = _remotePlaylist; - if (remotePlaylist == null) { - UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading)); - return@Pair; - } - - setLoading(true); - StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { - try { - StatePlaylists.instance.playlistStore.save(remotePlaylist.toPlaylist()); - - withContext(Dispatchers.Main) { - setLoading(false); - UIDialogs.toast(context.getString(R.string.playlist_copied_as_local_playlist)); - } - } catch (e: Throwable) { - withContext(Dispatchers.Main) { - setLoading(false); - } - - throw e; - } - } - })); - } - private fun fetchPlaylist() { Logger.i(TAG, "fetchPlaylist") @@ -290,21 +250,15 @@ class PlaylistFragment : MainFragment() { override fun onPlayAllClick() { val playlist = _playlist; - val remotePlaylist = _remotePlaylist; if (playlist != null) { StatePlayer.instance.setPlaylist(playlist, focus = true); - } else if (remotePlaylist != null) { - StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = false); } } override fun onShuffleClick() { val playlist = _playlist; - val remotePlaylist = _remotePlaylist; if (playlist != null) { StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true); - } else if (remotePlaylist != null) { - StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = true); } } @@ -320,19 +274,12 @@ class PlaylistFragment : MainFragment() { } override fun onVideoClicked(video: IPlatformVideo) { val playlist = _playlist; - val remotePlaylist = _remotePlaylist; if (playlist != null) { val index = playlist.videos.indexOf(video); if (index == -1) return; StatePlayer.instance.setPlaylist(playlist, index, true); - } else if (remotePlaylist != null) { - val index = remotePlaylist.contents.getResults().indexOf(video); - if (index == -1) - return; - - StatePlayer.instance.setPlaylist(remotePlaylist, index, true); } } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt new file mode 100644 index 00000000..84e06047 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/RemotePlaylistFragment.kt @@ -0,0 +1,362 @@ +package com.futo.platformplayer.fragment.mainactivity.main + +import android.annotation.SuppressLint +import android.graphics.drawable.Animatable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.app.ShareCompat +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.futo.platformplayer.* +import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist +import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails +import com.futo.platformplayer.api.media.models.video.IPlatformVideo +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.MultiPager +import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment +import com.futo.platformplayer.images.GlideHelper.Companion.crossfade +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.models.Playlist +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.states.StatePlaylists +import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader +import com.futo.platformplayer.views.adapters.VideoListEditorViewHolder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class RemotePlaylistFragment : MainFragment() { + override val isMainView : Boolean = true; + override val isTab: Boolean = true; + override val hasBottomBar: Boolean get() = true; + + private var _view: RemotePlaylistView? = null; + + override fun onShownWithView(parameter: Any?, isBack: Boolean) { + super.onShownWithView(parameter, isBack); + _view?.onShown(parameter); + } + + override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = RemotePlaylistView(this, inflater); + _view = view; + return view; + } + + override fun onDestroyMainView() { + super.onDestroyMainView(); + _view = null; + } + + @SuppressLint("ViewConstructor") + class RemotePlaylistView : LinearLayout { + private val _fragment: RemotePlaylistFragment; + + private var _remotePlaylist: IPlatformPlaylistDetails? = null; + private var _url: String? = null; + private val _videos: ArrayList = arrayListOf(); + + private val _taskLoadPlaylist: TaskHandler; + private var _nextPageHandler: TaskHandler, List>; + + private var _imagePlaylistThumbnail: ImageView; + private var _textName: TextView; + private var _textMetadata: TextView; + private var _loaderOverlay: FrameLayout; + private var _imageLoader: ImageView; + private var _overlayContainer: FrameLayout; + private var _buttonShare: ImageButton; + private var _recyclerPlaylist: RecyclerView; + private var _llmPlaylist: LinearLayoutManager; + private val _adapterVideos: InsertedViewAdapterWithLoader; + private val _scrollListener: RecyclerView.OnScrollListener + + constructor(fragment: RemotePlaylistFragment, inflater: LayoutInflater) : super(inflater.context) { + inflater.inflate(R.layout.fragment_remote_playlist, this); + + _fragment = fragment; + + _textName = findViewById(R.id.text_name); + _textMetadata = findViewById(R.id.text_metadata); + _imagePlaylistThumbnail = findViewById(R.id.image_playlist_thumbnail); + _loaderOverlay = findViewById(R.id.layout_loading_overlay); + _imageLoader = findViewById(R.id.image_loader); + _recyclerPlaylist = findViewById(R.id.recycler_playlist); + _llmPlaylist = LinearLayoutManager(context); + _adapterVideos = InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(), + childCountGetter = { _videos.size }, + childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_videos[position], false); }, + childViewHolderFactory = { viewGroup, _ -> + val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.list_playlist, viewGroup, false); + val holder = VideoListEditorViewHolder(view, null); + holder.onClick.subscribe { + showConvertConfirmationModal() { + _fragment.navigate(it); + } + }; + return@InsertedViewAdapterWithLoader holder; + } + ); + + _recyclerPlaylist.adapter = _adapterVideos; + _recyclerPlaylist.layoutManager = _llmPlaylist; + + _overlayContainer = findViewById(R.id.overlay_container); + val buttonPlayAll = findViewById(R.id.button_play_all); + val buttonShuffle = findViewById(R.id.button_shuffle); + + _buttonShare = findViewById(R.id.button_share); + _buttonShare.setOnClickListener { + val remotePlaylist = _remotePlaylist ?: return@setOnClickListener; + + _fragment.startActivity(ShareCompat.IntentBuilder(context) + .setType("text/plain") + .setText(remotePlaylist.shareUrl) + .intent); + }; + + buttonPlayAll.setOnClickListener { + showConvertConfirmationModal() { + _fragment.navigate(it); + } + }; + buttonShuffle.setOnClickListener { + showConvertConfirmationModal() { + _fragment.navigate(it); + } + }; + + _taskLoadPlaylist = TaskHandler( + StateApp.instance.scopeGetter, + { + return@TaskHandler StatePlatform.instance.getPlaylist(it); + }) + .success { + _remotePlaylist = it; + setName(it.name); + setVideos(it.contents.getResults()); + setVideoCount(it.videoCount); + setLoading(false); + } + .exception { + Logger.w(TAG, "Failed to load playlist.", it); + val c = context ?: return@exception; + UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist, null, fragment); + }; + + _nextPageHandler = TaskHandler, List>({fragment.lifecycleScope}, { + if (it is IAsyncPager<*>) + it.nextPageAsync(); + else + it.nextPage(); + + processPagerExceptions(it); + return@TaskHandler it.getResults(); + }).success { + _adapterVideos.setLoading(false); + addVideos(it); + //TODO: ensureEnoughContentVisible() + }.exception { + Logger.w(TAG, "Failed to load next page.", it); + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, { + loadNextPage(); + }, null, fragment); + }; + + _scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val visibleItemCount = _recyclerPlaylist.childCount + val firstVisibleItem = _llmPlaylist.findFirstVisibleItemPosition() + val visibleThreshold = 15 + if (!_adapterVideos.isLoading && firstVisibleItem + visibleItemCount + visibleThreshold >= _videos.size) { + loadNextPage() + } + } + } + + _recyclerPlaylist.addOnScrollListener(_scrollListener) + } + + private fun loadNextPage() { + val pager: IPager = _remotePlaylist?.contents ?: return; + val hasMorePages = pager.hasMorePages(); + Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}"); + + if (pager.hasMorePages()) { + _adapterVideos.setLoading(true); + _nextPageHandler.run(pager); + } + } + + private fun processPagerExceptions(pager: IPager<*>) { + if(pager is MultiPager<*> && pager.allowFailure) { + val ex = pager.getResultExceptions(); + for(kv in ex) { + val jsVideoPager: JSPager<*>? = if(kv.key is MultiPager<*>) + (kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>?; + else if(kv.key is JSPager<*>) + kv.key as JSPager<*>; + else null; + + context?.let { + _fragment.lifecycleScope.launch(Dispatchers.Main) { + try { + if(jsVideoPager != null) + UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", jsVideoPager.getPluginConfig().name).replace("{message}", kv.value.message ?: ""), false); + else + UIDialogs.toast(it, kv.value.message ?: "", false); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to show toast.", e) + } + } + } + } + } + } + + fun onShown(parameter: Any?) { + _taskLoadPlaylist.cancel(); + _nextPageHandler.cancel(); + + if (parameter is IPlatformPlaylist) { + _remotePlaylist = null; + _url = parameter.url; + + setVideoCount(parameter.videoCount); + setName(parameter.name); + setVideos(null); + + fetchPlaylist(); + showConvertPlaylistButton(); + } else if (parameter is String) { + _remotePlaylist = null; + _url = parameter; + + setName(null); + setVideos(null); + setVideoCount(-1); + + fetchPlaylist(); + showConvertPlaylistButton(); + } + } + + private fun showConvertConfirmationModal(onSuccess: ((playlist: Playlist) -> Unit)? = null) { + val remotePlaylist = _remotePlaylist; + if (remotePlaylist == null) { + UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading)); + return; + } + + val c = context ?: return; + UIDialogs.showConfirmationDialog(c, "Conversion to local playlist is required for this action", { + setLoading(true); + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + try { + val playlist = remotePlaylist.toPlaylist(); + StatePlaylists.instance.playlistStore.save(playlist); + + withContext(Dispatchers.Main) { + setLoading(false); + UIDialogs.toast(context.getString(R.string.playlist_copied_as_local_playlist)); + onSuccess?.invoke(playlist); + } + } catch (e: Throwable) { + withContext(Dispatchers.Main) { + setLoading(false); + } + + throw e; + } + } + }); + } + + private fun showConvertPlaylistButton() { + _fragment.topBar?.assume()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) { + showConvertConfirmationModal(); + })); + } + + private fun fetchPlaylist() { + Logger.i(TAG, "fetchPlaylist") + + val url = _url; + if (!url.isNullOrBlank()) { + setLoading(true); + _taskLoadPlaylist.run(url); + } + } + + private fun setName(name: String?) { + _textName.text = name ?: ""; + } + + private fun setVideoCount(videoCount: Int = -1) { + _textMetadata.text = if (videoCount == -1) "" else "${videoCount} " + context.getString(R.string.videos); + } + + private fun setVideos(videos: List?) { + if (!videos.isNullOrEmpty()) { + val video = videos.first(); + _imagePlaylistThumbnail.let { + Glide.with(it) + .load(video.thumbnails.getHQThumbnail()) + .placeholder(R.drawable.placeholder_video_thumbnail) + .crossfade() + .into(it); + }; + } else { + _textMetadata.text = "0 " + context.getString(R.string.videos); + Glide.with(_imagePlaylistThumbnail) + .load(R.drawable.placeholder_video_thumbnail) + .into(_imagePlaylistThumbnail) + } + + synchronized(_videos) { + _videos.clear(); + _videos.addAll(videos ?: listOf()); + _adapterVideos.notifyDataSetChanged(); + } + } + + private fun addVideos(videos: List) { + synchronized(_videos) { + val index = _videos.size; + _videos.addAll(videos); + _adapterVideos.notifyItemRangeInserted(_adapterVideos.childToParentPosition(index), videos.size); + } + } + + private fun setLoading(isLoading: Boolean) { + if (isLoading){ + (_imageLoader.drawable as Animatable?)?.start() + _loaderOverlay.visibility = View.VISIBLE; + } + else { + _loaderOverlay.visibility = View.GONE; + (_imageLoader.drawable as Animatable?)?.stop() + } + } + } + + companion object { + private const val TAG = "RemotePlaylistFragment"; + fun newInstance() = RemotePlaylistFragment().apply {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/InsertedViewAdapterWithLoader.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/InsertedViewAdapterWithLoader.kt index de012903..19989056 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/InsertedViewAdapterWithLoader.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/InsertedViewAdapterWithLoader.kt @@ -15,6 +15,7 @@ import com.futo.platformplayer.R open class InsertedViewAdapterWithLoader : InsertedViewAdapter where TViewHolder : ViewHolder { private var _loaderView: ImageView? = null; private var _loading = false; + val isLoading get() = _loading; constructor( context: Context, diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt index 8f94f6d2..3cf3194b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt @@ -43,7 +43,7 @@ class VideoListEditorViewHolder : ViewHolder { val onRemove = Event1(); @SuppressLint("ClickableViewAccessibility") - constructor(view: View, touchHelper: ItemTouchHelper) : super(view) { + constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) { _root = view.findViewById(R.id.root); _imageThumbnail = view.findViewById(R.id.image_video_thumbnail); _imageThumbnail?.clipToOutline = true; @@ -59,7 +59,7 @@ class VideoListEditorViewHolder : ViewHolder { _layoutDownloaded = view.findViewById(R.id.layout_downloaded); _imageDragDrop.setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { + if (touchHelper != null && event.action == MotionEvent.ACTION_DOWN) { touchHelper.startDrag(this); } false diff --git a/app/src/main/res/layout/fragment_remote_playlist.xml b/app/src/main/res/layout/fragment_remote_playlist.xml new file mode 100644 index 00000000..bee1f714 --- /dev/null +++ b/app/src/main/res/layout/fragment_remote_playlist.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +W + + + + + + + + + + + + + + + + + \ No newline at end of file