mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-02 22:30:40 +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 _fragMainTutorial: TutorialFragment;
|
||||||
lateinit var _fragMainPlaylists: PlaylistsFragment;
|
lateinit var _fragMainPlaylists: PlaylistsFragment;
|
||||||
lateinit var _fragMainPlaylist: PlaylistFragment;
|
lateinit var _fragMainPlaylist: PlaylistFragment;
|
||||||
|
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
|
||||||
lateinit var _fragWatchlist: WatchLaterFragment;
|
lateinit var _fragWatchlist: WatchLaterFragment;
|
||||||
lateinit var _fragHistory: HistoryFragment;
|
lateinit var _fragHistory: HistoryFragment;
|
||||||
lateinit var _fragSourceDetail: SourceDetailFragment;
|
lateinit var _fragSourceDetail: SourceDetailFragment;
|
||||||
|
@ -246,6 +247,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
_fragMainSources = SourcesFragment.newInstance();
|
_fragMainSources = SourcesFragment.newInstance();
|
||||||
_fragMainPlaylists = PlaylistsFragment.newInstance();
|
_fragMainPlaylists = PlaylistsFragment.newInstance();
|
||||||
_fragMainPlaylist = PlaylistFragment.newInstance();
|
_fragMainPlaylist = PlaylistFragment.newInstance();
|
||||||
|
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
||||||
_fragPostDetail = PostDetailFragment.newInstance();
|
_fragPostDetail = PostDetailFragment.newInstance();
|
||||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||||
_fragHistory = HistoryFragment.newInstance();
|
_fragHistory = HistoryFragment.newInstance();
|
||||||
|
@ -331,6 +333,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
_fragMainSources.topBar = _fragTopBarAdd;
|
_fragMainSources.topBar = _fragTopBarAdd;
|
||||||
_fragMainPlaylists.topBar = _fragTopBarGeneral;
|
_fragMainPlaylists.topBar = _fragTopBarGeneral;
|
||||||
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
||||||
|
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
||||||
_fragPostDetail.topBar = _fragTopBarNavigation;
|
_fragPostDetail.topBar = _fragTopBarNavigation;
|
||||||
_fragWatchlist.topBar = _fragTopBarNavigation;
|
_fragWatchlist.topBar = _fragTopBarNavigation;
|
||||||
_fragHistory.topBar = _fragTopBarNavigation;
|
_fragHistory.topBar = _fragTopBarNavigation;
|
||||||
|
@ -1044,6 +1047,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
SourcesFragment::class -> _fragMainSources as T;
|
SourcesFragment::class -> _fragMainSources as T;
|
||||||
PlaylistsFragment::class -> _fragMainPlaylists as T;
|
PlaylistsFragment::class -> _fragMainPlaylists as T;
|
||||||
PlaylistFragment::class -> _fragMainPlaylist as T;
|
PlaylistFragment::class -> _fragMainPlaylist as T;
|
||||||
|
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
||||||
PostDetailFragment::class -> _fragPostDetail as T;
|
PostDetailFragment::class -> _fragPostDetail as T;
|
||||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||||
HistoryFragment::class -> _fragHistory as T;
|
HistoryFragment::class -> _fragHistory as T;
|
||||||
|
|
|
@ -204,7 +204,7 @@ class ChannelFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
is IPlatformPlaylist -> {
|
is IPlatformPlaylist -> {
|
||||||
fragment.navigate<PlaylistFragment>(v)
|
fragment.navigate<RemotePlaylistFragment>(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
is IPlatformPost -> {
|
is IPlatformPost -> {
|
||||||
|
|
|
@ -6,28 +6,32 @@ import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.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.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
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.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
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.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
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.PreviewNestedVideoViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.withTimestamp
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
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();
|
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
|
||||||
}
|
}
|
||||||
} else if (content is IPlatformPlaylist) {
|
} else if (content is IPlatformPlaylist) {
|
||||||
fragment.navigate<PlaylistFragment>(content);
|
fragment.navigate<RemotePlaylistFragment>(content);
|
||||||
} else if (content is IPlatformPost) {
|
} else if (content is IPlatformPost) {
|
||||||
fragment.navigate<PostDetailFragment>(content);
|
fragment.navigate<PostDetailFragment>(content);
|
||||||
}
|
}
|
||||||
|
@ -194,7 +198,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||||
StatePlayer.instance.clearQueue();
|
StatePlayer.instance.clearQueue();
|
||||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
|
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
|
||||||
};
|
};
|
||||||
ContentType.PLAYLIST -> fragment.navigate<PlaylistFragment>(url);
|
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url);
|
||||||
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
|
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
|
||||||
else -> {};
|
else -> {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
onSearch.subscribe(this) {
|
onSearch.subscribe(this) {
|
||||||
if(it.isHttpUrl()) {
|
if(it.isHttpUrl()) {
|
||||||
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||||
navigate<PlaylistFragment>(it);
|
navigate<RemotePlaylistFragment>(it);
|
||||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
navigate<ChannelFragment>(it);
|
navigate<ChannelFragment>(it);
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.view.setPadding
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
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.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.downloads.VideoDownload
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.StateApp
|
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 com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class PlaylistFragment : MainFragment() {
|
class PlaylistFragment : MainFragment() {
|
||||||
override val isMainView : Boolean = true;
|
override val isMainView : Boolean = true;
|
||||||
|
@ -70,7 +65,6 @@ class PlaylistFragment : MainFragment() {
|
||||||
private val _fragment: PlaylistFragment;
|
private val _fragment: PlaylistFragment;
|
||||||
|
|
||||||
private var _playlist: Playlist? = null;
|
private var _playlist: Playlist? = null;
|
||||||
private var _remotePlaylist: IPlatformPlaylistDetails? = null;
|
|
||||||
private var _editPlaylistNameInput: SlideUpMenuTextInput? = null;
|
private var _editPlaylistNameInput: SlideUpMenuTextInput? = null;
|
||||||
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
|
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
|
||||||
private var _url: String? = null;
|
private var _url: String? = null;
|
||||||
|
@ -136,7 +130,6 @@ class PlaylistFragment : MainFragment() {
|
||||||
return@TaskHandler StatePlatform.instance.getPlaylist(it);
|
return@TaskHandler StatePlatform.instance.getPlaylist(it);
|
||||||
})
|
})
|
||||||
.success {
|
.success {
|
||||||
_remotePlaylist = it;
|
|
||||||
setName(it.name);
|
setName(it.name);
|
||||||
//TODO: Implement support for pagination
|
//TODO: Implement support for pagination
|
||||||
setVideos(it.toPlaylist().videos, false);
|
setVideos(it.toPlaylist().videos, false);
|
||||||
|
@ -155,7 +148,6 @@ class PlaylistFragment : MainFragment() {
|
||||||
|
|
||||||
if (parameter is Playlist?) {
|
if (parameter is Playlist?) {
|
||||||
_playlist = parameter;
|
_playlist = parameter;
|
||||||
_remotePlaylist = null;
|
|
||||||
_url = null;
|
_url = null;
|
||||||
|
|
||||||
if(parameter != null) {
|
if(parameter != null) {
|
||||||
|
@ -175,7 +167,6 @@ class PlaylistFragment : MainFragment() {
|
||||||
//TODO: Do I have to remove the showConvertPlaylistButton(); button here?
|
//TODO: Do I have to remove the showConvertPlaylistButton(); button here?
|
||||||
} else if (parameter is IPlatformPlaylist) {
|
} else if (parameter is IPlatformPlaylist) {
|
||||||
_playlist = null;
|
_playlist = null;
|
||||||
_remotePlaylist = null;
|
|
||||||
_url = parameter.url;
|
_url = parameter.url;
|
||||||
|
|
||||||
setVideoCount(parameter.videoCount);
|
setVideoCount(parameter.videoCount);
|
||||||
|
@ -185,10 +176,8 @@ class PlaylistFragment : MainFragment() {
|
||||||
setButtonEditVisible(false);
|
setButtonEditVisible(false);
|
||||||
|
|
||||||
fetchPlaylist();
|
fetchPlaylist();
|
||||||
showConvertPlaylistButton();
|
|
||||||
} else if (parameter is String) {
|
} else if (parameter is String) {
|
||||||
_playlist = null;
|
_playlist = null;
|
||||||
_remotePlaylist = null;
|
|
||||||
_url = parameter;
|
_url = parameter;
|
||||||
|
|
||||||
setName(null);
|
setName(null);
|
||||||
|
@ -198,7 +187,6 @@ class PlaylistFragment : MainFragment() {
|
||||||
setButtonEditVisible(false);
|
setButtonEditVisible(false);
|
||||||
|
|
||||||
fetchPlaylist();
|
fetchPlaylist();
|
||||||
showConvertPlaylistButton();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_playlist?.let {
|
_playlist?.let {
|
||||||
|
@ -242,34 +230,6 @@ class PlaylistFragment : MainFragment() {
|
||||||
StateDownloads.instance.onDownloadedChanged.remove(this);
|
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() {
|
private fun fetchPlaylist() {
|
||||||
Logger.i(TAG, "fetchPlaylist")
|
Logger.i(TAG, "fetchPlaylist")
|
||||||
|
|
||||||
|
@ -290,21 +250,15 @@ class PlaylistFragment : MainFragment() {
|
||||||
|
|
||||||
override fun onPlayAllClick() {
|
override fun onPlayAllClick() {
|
||||||
val playlist = _playlist;
|
val playlist = _playlist;
|
||||||
val remotePlaylist = _remotePlaylist;
|
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
StatePlayer.instance.setPlaylist(playlist, focus = true);
|
StatePlayer.instance.setPlaylist(playlist, focus = true);
|
||||||
} else if (remotePlaylist != null) {
|
|
||||||
StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShuffleClick() {
|
override fun onShuffleClick() {
|
||||||
val playlist = _playlist;
|
val playlist = _playlist;
|
||||||
val remotePlaylist = _remotePlaylist;
|
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true);
|
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) {
|
override fun onVideoClicked(video: IPlatformVideo) {
|
||||||
val playlist = _playlist;
|
val playlist = _playlist;
|
||||||
val remotePlaylist = _remotePlaylist;
|
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
val index = playlist.videos.indexOf(video);
|
val index = playlist.videos.indexOf(video);
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
StatePlayer.instance.setPlaylist(playlist, index, true);
|
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 {
|
open class InsertedViewAdapterWithLoader<TViewHolder> : InsertedViewAdapter<TViewHolder> where TViewHolder : ViewHolder {
|
||||||
private var _loaderView: ImageView? = null;
|
private var _loaderView: ImageView? = null;
|
||||||
private var _loading = false;
|
private var _loading = false;
|
||||||
|
val isLoading get() = _loading;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
|
@ -43,7 +43,7 @@ class VideoListEditorViewHolder : ViewHolder {
|
||||||
val onRemove = Event1<IPlatformVideo>();
|
val onRemove = Event1<IPlatformVideo>();
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
constructor(view: View, touchHelper: ItemTouchHelper) : super(view) {
|
constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) {
|
||||||
_root = view.findViewById(R.id.root);
|
_root = view.findViewById(R.id.root);
|
||||||
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
|
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
|
||||||
_imageThumbnail?.clipToOutline = true;
|
_imageThumbnail?.clipToOutline = true;
|
||||||
|
@ -59,7 +59,7 @@ class VideoListEditorViewHolder : ViewHolder {
|
||||||
_layoutDownloaded = view.findViewById(R.id.layout_downloaded);
|
_layoutDownloaded = view.findViewById(R.id.layout_downloaded);
|
||||||
|
|
||||||
_imageDragDrop.setOnTouchListener { _, event ->
|
_imageDragDrop.setOnTouchListener { _, event ->
|
||||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
if (touchHelper != null && event.action == MotionEvent.ACTION_DOWN) {
|
||||||
touchHelper.startDrag(this);
|
touchHelper.startDrag(this);
|
||||||
}
|
}
|
||||||
false
|
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
Add a link
Reference in a new issue