mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-18 13:52:39 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 486ebd6bc8 | |||
| 74b9926647 | |||
| 2a6ba6d541 | |||
| 931216ab7d | |||
| 916936e179 | |||
| b535353365 | |||
| be2ae096ee | |||
| 948b85ddcb | |||
| ae904b4cd8 | |||
| aad50e7b50 | |||
| ff28a07871 |
@@ -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;
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package com.futo.platformplayer.api.media
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
|
||||
/**
|
||||
* A temporary class that caches video results
|
||||
* In future this should be part of a bigger system
|
||||
*/
|
||||
class CachedPlatformClient : IPlatformClient {
|
||||
private val _client : IPlatformClient;
|
||||
override val id: String get() = _client.id;
|
||||
override val name: String get() = _client.name;
|
||||
override val icon: ImageVariable? get() = _client.icon;
|
||||
|
||||
private val _cache: LruCache<String, IPlatformContentDetails>;
|
||||
|
||||
override val capabilities: PlatformClientCapabilities
|
||||
get() = _client.capabilities;
|
||||
|
||||
constructor(client : IPlatformClient, cacheSize : Int = 10 * 1024 * 1024) {
|
||||
this._client = client;
|
||||
this._cache = LruCache<String, IPlatformContentDetails>(cacheSize);
|
||||
}
|
||||
override fun initialize() { _client.initialize() }
|
||||
override fun disable() { _client.disable() }
|
||||
|
||||
override fun isContentDetailsUrl(url: String): Boolean = _client.isContentDetailsUrl(url);
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails {
|
||||
var result = _cache.get(url);
|
||||
if(result == null) {
|
||||
result = _client.getContentDetails(url);
|
||||
_cache.put(url, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
|
||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
|
||||
|
||||
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
|
||||
override fun getChannel(channelUrl: String): IPlatformChannel = _client.getChannel(channelUrl);
|
||||
|
||||
override fun getChannelCapabilities(): ResultCapabilities = _client.getChannelCapabilities();
|
||||
override fun getChannelContents(
|
||||
channelUrl: String,
|
||||
type: String?,
|
||||
order: String?,
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
||||
|
||||
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> = _client.getChannelPlaylists(channelUrl);
|
||||
|
||||
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
|
||||
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
|
||||
|
||||
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
||||
|
||||
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
||||
override fun getSearchCapabilities(): ResultCapabilities = _client.getSearchCapabilities();
|
||||
override fun search(
|
||||
query: String,
|
||||
type: String?,
|
||||
order: String?,
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.search(query, type, order, filters);
|
||||
|
||||
override fun getSearchChannelContentsCapabilities(): ResultCapabilities = _client.getSearchChannelContentsCapabilities();
|
||||
override fun searchChannelContents(
|
||||
channelUrl: String,
|
||||
query: String,
|
||||
type: String?,
|
||||
order: String?,
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.searchChannelContents(channelUrl, query, type, order, filters);
|
||||
|
||||
override fun searchChannels(query: String) = _client.searchChannels(query);
|
||||
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = _client.getComments(url);
|
||||
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> = _client.getSubComments(comment);
|
||||
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = _client.getLiveChatWindow(url);
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = _client.getLiveEvents(url);
|
||||
|
||||
override fun getHome(): IPager<IPlatformContent> = _client.getHome();
|
||||
|
||||
override fun getUserSubscriptions(): Array<String> { return arrayOf(); };
|
||||
|
||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = _client.searchPlaylists(query, type, order, filters);
|
||||
override fun isPlaylistUrl(url: String): Boolean = _client.isPlaylistUrl(url);
|
||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = _client.getPlaylist(url);
|
||||
override fun getUserPlaylists(): Array<String> { return arrayOf(); };
|
||||
|
||||
override fun isClaimTypeSupported(claimType: Int): Boolean {
|
||||
return _client.isClaimTypeSupported(claimType);
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,11 @@ interface IPlatformClient {
|
||||
*/
|
||||
fun getPlaybackTracker(url: String): IPlaybackTracker?;
|
||||
|
||||
/**
|
||||
* Get content recommendations
|
||||
*/
|
||||
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
|
||||
|
||||
|
||||
//Comments
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,8 @@ data class PlatformClientCapabilities(
|
||||
val hasGetLiveChatWindow: Boolean = false,
|
||||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false,
|
||||
val hasGetChannelPlaylists: Boolean = false
|
||||
val hasGetChannelPlaylists: Boolean = false,
|
||||
val hasGetContentRecommendations: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
||||
+2
@@ -10,4 +10,6 @@ interface IPlatformContentDetails : IPlatformContent {
|
||||
|
||||
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
|
||||
fun getPlaybackTracker(): IPlaybackTracker?;
|
||||
|
||||
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
|
||||
}
|
||||
+1
-1
@@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
|
||||
//TODO: Determine if this should be IPlatformContent (probably not?)
|
||||
val contents: IPager<IPlatformVideo>;
|
||||
|
||||
fun toPlaylist(): Playlist;
|
||||
fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
|
||||
}
|
||||
+2
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
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.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||
@@ -56,6 +57,7 @@ open class SerializedPlatformVideoDetails(
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||
|
||||
companion object {
|
||||
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {
|
||||
|
||||
@@ -560,7 +560,7 @@ open class JSClient : IPlatformClient {
|
||||
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
||||
}
|
||||
|
||||
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||
@JSDocs(18, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||
@JSDocsParameter("url", "Url of live stream")
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||
if(!capabilities.hasGetLiveChatWindow)
|
||||
@@ -569,7 +569,7 @@ open class JSClient : IPlatformClient {
|
||||
return@isBusyWith JSLiveChatWindowDescriptor(config,
|
||||
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
|
||||
}
|
||||
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||
@JSDocs(19, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||
@JSDocsParameter("url", "Url of live stream")
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||
if(!capabilities.hasGetLiveEvents)
|
||||
@@ -578,6 +578,20 @@ open class JSClient : IPlatformClient {
|
||||
return@isBusyWith JSLiveEventPager(config, this,
|
||||
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
||||
}
|
||||
|
||||
|
||||
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
|
||||
@JSDocsParameter("url", "Url of content")
|
||||
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
|
||||
if(!capabilities.hasGetContentRecommendations)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.getContentRecommendations(${Json.encodeToString(url)})"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
||||
@JSDocsParameter("query", "Query that search results should match")
|
||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||
|
||||
+11
-7
@@ -7,7 +7,7 @@ import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.api.media.structures.ReusablePager
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
|
||||
@@ -15,22 +15,26 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
|
||||
override val contents: IPager<IPlatformVideo>;
|
||||
|
||||
constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||
contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
|
||||
contents = ReusablePager(JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails")));
|
||||
}
|
||||
|
||||
override fun toPlaylist(): Playlist {
|
||||
val videos = contents.getResults().toMutableList();
|
||||
override fun toPlaylist(onProgress: ((progress: Int) -> Unit)?): Playlist {
|
||||
val playlist = if (contents is ReusablePager) contents.getWindow() else contents;
|
||||
val videos = playlist.getResults().toMutableList();
|
||||
onProgress?.invoke(videos.size);
|
||||
|
||||
//Download all pages
|
||||
var allowedEmptyCount = 2;
|
||||
while(contents.hasMorePages()) {
|
||||
contents.nextPage();
|
||||
if(!videos.addAll(contents.getResults())) {
|
||||
while(playlist.hasMorePages()) {
|
||||
playlist.nextPage();
|
||||
if(!videos.addAll(playlist.getResults())) {
|
||||
allowedEmptyCount--;
|
||||
if(allowedEmptyCount <= 0)
|
||||
break;
|
||||
}
|
||||
else allowedEmptyCount = 2;
|
||||
|
||||
onProgress?.invoke(videos.size);
|
||||
}
|
||||
|
||||
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
||||
|
||||
+21
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
|
||||
@@ -18,6 +19,7 @@ import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
private val _hasGetComments: Boolean;
|
||||
private val _hasGetContentRecommendations: Boolean;
|
||||
|
||||
override val rating: IRating;
|
||||
|
||||
@@ -34,6 +36,7 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
||||
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
@@ -51,9 +54,27 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||
}
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
|
||||
return@handleDevCall getContentRecommendationsJS(client);
|
||||
}
|
||||
else if(client is JSClient)
|
||||
return getContentRecommendationsJS(client);
|
||||
|
||||
return null;
|
||||
}
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -6,6 +6,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
@@ -27,6 +28,7 @@ import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
private val _hasGetComments: Boolean;
|
||||
private val _hasGetContentRecommendations: Boolean;
|
||||
private val _hasGetPlaybackTracker: Boolean;
|
||||
|
||||
//Details
|
||||
@@ -66,6 +68,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
}
|
||||
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||
@@ -89,6 +92,24 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
};
|
||||
}
|
||||
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getContentRecommendations()") {
|
||||
return@handleDevCall getContentRecommendationsJS(client);
|
||||
}
|
||||
else if(client is JSClient)
|
||||
return getContentRecommendationsJS(client);
|
||||
|
||||
return null;
|
||||
}
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
if(client !is JSClient || !_hasGetComments || _content.isClosed)
|
||||
return null;
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
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.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
@@ -81,6 +82,8 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||
|
||||
|
||||
fun toPlatformVideo() : IPlatformVideoDetails {
|
||||
throw NotImplementedError();
|
||||
|
||||
+1
-1
@@ -204,7 +204,7 @@ class ChannelFragment : MainFragment() {
|
||||
}
|
||||
|
||||
is IPlatformPlaylist -> {
|
||||
fragment.navigate<PlaylistFragment>(v)
|
||||
fragment.navigate<RemotePlaylistFragment>(v)
|
||||
}
|
||||
|
||||
is IPlatformPost -> {
|
||||
|
||||
+9
-5
@@ -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 -> {};
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+33
-8
@@ -12,16 +12,21 @@ import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.adapters.viewholders.ImportPlaylistsViewHolder
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ImportPlaylistsFragment : MainFragment() {
|
||||
override val isMainView : Boolean = true;
|
||||
@@ -67,7 +72,7 @@ class ImportPlaylistsFragment : MainFragment() {
|
||||
private val _items: ArrayList<SelectablePlaylist> = arrayListOf();
|
||||
private var _currentLoadIndex = 0;
|
||||
|
||||
private var _taskLoadPlaylist: TaskHandler<String, Playlist?>;
|
||||
private var _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails?>;
|
||||
|
||||
constructor(fragment: ImportPlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||
_fragment = fragment;
|
||||
@@ -102,7 +107,7 @@ class ImportPlaylistsFragment : MainFragment() {
|
||||
|
||||
setLoading(false);
|
||||
|
||||
_taskLoadPlaylist = TaskHandler<String, Playlist?>({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link).toPlaylist(); })
|
||||
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails?>({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link); })
|
||||
.success {
|
||||
if (it != null) {
|
||||
_items.add(SelectablePlaylist(it));
|
||||
@@ -113,7 +118,7 @@ class ImportPlaylistsFragment : MainFragment() {
|
||||
}.exceptionWithParameter<Throwable> { ex, para ->
|
||||
//setLoading(false);
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
||||
UIDialogs.toast(context, context.getString(R.string.failed_to_fetch) + "\n${para}", false)
|
||||
UIDialogs.appToast(context.getString(R.string.failed_to_fetch) + "\n${para}\n" + ex.message, false)
|
||||
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
|
||||
loadNext();
|
||||
};
|
||||
@@ -147,12 +152,32 @@ class ImportPlaylistsFragment : MainFragment() {
|
||||
it.title = context.getString(R.string.import_playlists);
|
||||
it.onImport.subscribe(this) {
|
||||
val playlistsToImport = _items.filter { i -> i.selected }.toList();
|
||||
for (playlistToImport in playlistsToImport) {
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist);
|
||||
}
|
||||
|
||||
UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported));
|
||||
_fragment.closeSegment();
|
||||
UIDialogs.showDialogProgress(context) {
|
||||
it.setText("Importing playlists..");
|
||||
it.setProgress(0f);
|
||||
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
for ((i, playlistToImport) in playlistsToImport.withIndex()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setText("Importing playlists..\n[${playlistToImport.playlist.name}]");
|
||||
}
|
||||
try {
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist.toPlaylist());
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
UIDialogs.appToast("Failed to import [${playlistToImport.playlist.name}]\n" + ex.message);
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(i.toDouble() / playlistsToImport.size);
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported));
|
||||
_fragment.closeSegment();
|
||||
it.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+3
-56
@@ -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,12 +130,11 @@ class PlaylistFragment : MainFragment() {
|
||||
return@TaskHandler StatePlatform.instance.getPlaylist(it);
|
||||
})
|
||||
.success {
|
||||
setLoading(false);
|
||||
_remotePlaylist = it;
|
||||
setName(it.name);
|
||||
setVideos(it.contents.getResults(), false);
|
||||
setVideoCount(it.videoCount);
|
||||
//TODO: Implement support for pagination
|
||||
setVideos(it.toPlaylist().videos, false);
|
||||
setVideoCount(it.videoCount);
|
||||
setLoading(false);
|
||||
}
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load playlist.", it);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
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.api.media.structures.ReusablePager
|
||||
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.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 _remotePlaylistPagerWindow: IPager<IPlatformVideo>? = 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();
|
||||
};
|
||||
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();
|
||||
};
|
||||
buttonShuffle.setOnClickListener {
|
||||
showConvertConfirmationModal();
|
||||
};
|
||||
|
||||
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{
|
||||
return@TaskHandler StatePlatform.instance.getPlaylist(it);
|
||||
})
|
||||
.success {
|
||||
_remotePlaylist = it;
|
||||
val c = it.contents;
|
||||
_remotePlaylistPagerWindow = if (c is ReusablePager) c.getWindow() else c;
|
||||
setName(it.name);
|
||||
setVideos(_remotePlaylistPagerWindow!!.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> = _remotePlaylistPagerWindow ?: 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() {
|
||||
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);
|
||||
|
||||
UIDialogs.showDialogProgress(context) {
|
||||
it.setText("Converting playlist..");
|
||||
it.setProgress(0f);
|
||||
|
||||
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val playlist = remotePlaylist.toPlaylist() { progress ->
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
it.setProgress(progress.toDouble() / remotePlaylist.videoCount);
|
||||
}
|
||||
};
|
||||
|
||||
StatePlaylists.instance.playlistStore.save(playlist);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast("Playlist converted");
|
||||
it.dismiss();
|
||||
_fragment.navigate<PlaylistFragment>(playlist);
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
UIDialogs.appToast("Failed to convert playlist.\n" + ex.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.models.Thumbnail
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
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.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
@@ -144,10 +145,8 @@ class TutorialFragment : MainFragment() {
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
return EmptyPager()
|
||||
}
|
||||
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||
return null
|
||||
}
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+5
@@ -1063,6 +1063,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
if(!bypassSameVideoCheck && this.video?.url == video.url)
|
||||
return;
|
||||
//Loop workaround
|
||||
if(bypassSameVideoCheck && this.video?.url == video.url && StatePlayer.instance.loopVideo) {
|
||||
_player.seekTo(0);
|
||||
return;
|
||||
}
|
||||
|
||||
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
||||
if(cachedVideo != null) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.serializers
|
||||
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformLockedContent
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformNestedContent
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
@@ -30,6 +31,7 @@ class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPla
|
||||
"NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
|
||||
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
|
||||
"POST" -> SerializedPlatformPost.serializer();
|
||||
"LOCKED" -> SerializedPlatformLockedContent.serializer();
|
||||
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
|
||||
};
|
||||
} else {
|
||||
@@ -38,6 +40,7 @@ class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPla
|
||||
ContentType.NESTED_VIDEO.value -> SerializedPlatformNestedContent.serializer();
|
||||
ContentType.ARTICLE.value -> throw NotImplementedError("Articles not yet implemented");
|
||||
ContentType.POST.value -> SerializedPlatformPost.serializer();
|
||||
ContentType.LOCKED.value -> SerializedPlatformLockedContent.serializer();
|
||||
else -> throw NotImplementedError("Unknown Content Type Value: ${obj.jsonPrimitive.int}")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -647,6 +647,15 @@ class StatePlatform {
|
||||
return client.getPlaybackTracker(url);
|
||||
}
|
||||
|
||||
fun getContentRecommendations(url: String): IPager<IPlatformContent>? {
|
||||
val baseClient = getContentClientOrNull(url) ?: return null;
|
||||
if (baseClient !is JSClient) {
|
||||
return baseClient.getContentRecommendations(url);
|
||||
}
|
||||
val client = _mainClientPool.getClientPooled(baseClient);
|
||||
return client.getContentRecommendations(url);
|
||||
}
|
||||
|
||||
fun hasEnabledChannelClient(url : String) : Boolean = getEnabledClients().any { it.isChannelUrl(url) };
|
||||
fun getChannelClient(url : String, exclude: List<String>? = null) : IPlatformClient = getChannelClientOrNull(url, exclude)
|
||||
?: throw NoPlatformClientException("No client enabled that supports this channel url (${url})");
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+4
-3
@@ -7,6 +7,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
@@ -45,10 +46,10 @@ class ImportPlaylistsViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||
|
||||
override fun bind(value: SelectablePlaylist) {
|
||||
_textName.text = value.playlist.name;
|
||||
_textMetadata.text = "${value.playlist.videos.size} " + _view.context.getString(R.string.videos);
|
||||
_textMetadata.text = "${value.playlist.videoCount} " + _view.context.getString(R.string.videos);
|
||||
_checkbox.value = value.selected;
|
||||
|
||||
val thumbnail = value.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail();
|
||||
val thumbnail = value.playlist.thumbnail;
|
||||
if (thumbnail != null)
|
||||
Glide.with(_imageThumbnail)
|
||||
.load(thumbnail)
|
||||
@@ -62,6 +63,6 @@ class ImportPlaylistsViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||
}
|
||||
|
||||
class SelectablePlaylist(
|
||||
val playlist: Playlist,
|
||||
val playlist: IPlatformPlaylistDetails,
|
||||
var selected: Boolean = false
|
||||
) { }
|
||||
@@ -45,6 +45,7 @@
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
@@ -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>
|
||||
Submodule app/src/stable/assets/sources/spotify updated: 843cf2dc4b...4e826dcb6a
Submodule app/src/stable/assets/sources/youtube updated: c23302da76...5032e4e10a
Submodule app/src/unstable/assets/sources/spotify updated: 843cf2dc4b...4e826dcb6a
Submodule app/src/unstable/assets/sources/youtube updated: c23302da76...5032e4e10a
Reference in New Issue
Block a user