mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-19 19:14:51 +00:00
Implemented proper remote playlist support.
This commit is contained in:
parent
948b85ddcb
commit
916936e179
9 changed files with 578 additions and 62 deletions
|
@ -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;
|
||||
|
|
|
@ -204,7 +204,7 @@ class ChannelFragment : MainFragment() {
|
|||
}
|
||||
|
||||
is IPlatformPlaylist -> {
|
||||
fragment.navigate<PlaylistFragment>(v)
|
||||
fragment.navigate<RemotePlaylistFragment>(v)
|
||||
}
|
||||
|
||||
is IPlatformPost -> {
|
||||
|
|
|
@ -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<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
||||
|
@ -183,7 +187,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
|
||||
}
|
||||
} else if (content is IPlatformPlaylist) {
|
||||
fragment.navigate<PlaylistFragment>(content);
|
||||
fragment.navigate<RemotePlaylistFragment>(content);
|
||||
} else if (content is IPlatformPost) {
|
||||
fragment.navigate<PostDetailFragment>(content);
|
||||
}
|
||||
|
@ -194,7 +198,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||
StatePlayer.instance.clearQueue();
|
||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
|
||||
};
|
||||
ContentType.PLAYLIST -> fragment.navigate<PlaylistFragment>(url);
|
||||
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url);
|
||||
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
|
||||
else -> {};
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||
onSearch.subscribe(this) {
|
||||
if(it.isHttpUrl()) {
|
||||
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||
navigate<PlaylistFragment>(it);
|
||||
navigate<RemotePlaylistFragment>(it);
|
||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||
navigate<ChannelFragment>(it);
|
||||
else
|
||||
|
|
|
@ -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<NavigationTopBarFragment>()?.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<IPlatformVideo> = arrayListOf();
|
||||
|
||||
private val _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails>;
|
||||
private var _nextPageHandler: TaskHandler<IPager<IPlatformVideo>, List<IPlatformVideo>>;
|
||||
|
||||
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<VideoListEditorViewHolder>;
|
||||
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<PlaylistFragment>(it);
|
||||
}
|
||||
};
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
);
|
||||
|
||||
_recyclerPlaylist.adapter = _adapterVideos;
|
||||
_recyclerPlaylist.layoutManager = _llmPlaylist;
|
||||
|
||||
_overlayContainer = findViewById(R.id.overlay_container);
|
||||
val buttonPlayAll = findViewById<LinearLayout>(R.id.button_play_all);
|
||||
val buttonShuffle = findViewById<LinearLayout>(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<PlaylistFragment>(it);
|
||||
}
|
||||
};
|
||||
buttonShuffle.setOnClickListener {
|
||||
showConvertConfirmationModal() {
|
||||
_fragment.navigate<PlaylistFragment>(it);
|
||||
}
|
||||
};
|
||||
|
||||
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>(
|
||||
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<Throwable> {
|
||||
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<IPager<IPlatformVideo>, List<IPlatformVideo>>({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<Throwable> {
|
||||
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<IPlatformVideo> = _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<NavigationTopBarFragment>()?.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<IPlatformVideo>?) {
|
||||
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<IPlatformVideo>) {
|
||||
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 {}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import com.futo.platformplayer.R
|
|||
open class InsertedViewAdapterWithLoader<TViewHolder> : InsertedViewAdapter<TViewHolder> where TViewHolder : ViewHolder {
|
||||
private var _loaderView: ImageView? = null;
|
||||
private var _loading = false;
|
||||
val isLoading get() = _loading;
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
|
|
|
@ -43,7 +43,7 @@ class VideoListEditorViewHolder : ViewHolder {
|
|||
val onRemove = Event1<IPlatformVideo>();
|
||||
|
||||
@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
|
||||
|
|
198
app/src/main/res/layout/fragment_remote_playlist.xml
Normal file
198
app/src/main/res/layout/fragment_remote_playlist.xml
Normal file
|
@ -0,0 +1,198 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
app:elevation="0dp">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="0dp"
|
||||
app:layout_scrollFlags="scroll"
|
||||
app:contentInsetStart="0dp"
|
||||
app:contentInsetEnd="0dp">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="220dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_playlist_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:srcCompat="@drawable/background_thumbnail_live"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:srcCompat="@drawable/bottom_gradient"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_share"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/background_button_round"
|
||||
android:gravity="center"
|
||||
android:layout_marginStart="5dp"
|
||||
android:orientation="horizontal"
|
||||
app:srcCompat="@drawable/ic_share"
|
||||
app:tint="@color/white"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_margin="10dp"
|
||||
android:scaleType="fitCenter" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:layout_marginTop="-90dp"
|
||||
android:layout_marginStart="20dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<TextView
|
||||
android:id="@+id/text_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_medium"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18dp"
|
||||
tools:text="Playlist name"
|
||||
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_metadata"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
android:textColor="@color/gray_e0"
|
||||
android:textSize="14dp"
|
||||
tools:text="3 videos"
|
||||
android:layout_marginBottom="15dp"
|
||||
app:layout_constraintLeft_toLeftOf="@id/container_buttons"
|
||||
app:layout_constraintBottom_toTopOf="@id/container_buttons" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_buttons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginStart="10dp"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:id="@+id/button_play_all"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/background_button_primary_round"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_play_white_nopad"
|
||||
android:layout_marginEnd="10dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16dp"
|
||||
android:text="@string/play_all" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_shuffle"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/background_button_round"
|
||||
android:gravity="center"
|
||||
android:layout_marginStart="5dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_shuffle"
|
||||
android:layout_marginEnd="5dp"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16dp"
|
||||
android:text="@string/shuffle" />
|
||||
</LinearLayout>
|
||||
W
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_playlist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_loading_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#77000000"
|
||||
android:visibility="gone">
|
||||
<ImageView
|
||||
android:id="@+id/image_loader"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
app:srcCompat="@drawable/ic_loader_animated"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0.7"
|
||||
android:layout_marginTop="80dp"
|
||||
android:contentDescription="@string/loading" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
Loading…
Add table
Reference in a new issue