Compare commits

...

11 Commits

Author SHA1 Message Date
Kelvin K 486ebd6bc8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-19 19:14:20 +02:00
Kelvin K 74b9926647 Refs 2024-06-19 19:14:05 +02:00
Koen 2a6ba6d541 Fixed remote playlist ToPlaylist. 2024-06-14 14:54:37 +02:00
Koen 931216ab7d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-14 13:32:10 +02:00
Koen 916936e179 Implemented proper remote playlist support. 2024-06-14 13:32:00 +02:00
Kelvin K b535353365 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-06-14 13:23:10 +02:00
Kelvin K be2ae096ee Fix locked content deserializer 2024-06-14 13:22:58 +02:00
Koen 948b85ddcb Pushed updated submodules. 2024-06-14 08:43:18 +02:00
Kelvin K ae904b4cd8 Content recommendation api support, removing old CachedPlatformClient 2024-06-13 17:46:22 +02:00
Kelvin K aad50e7b50 Improved playlist import support 2024-06-13 13:45:31 +02:00
Kelvin K ff28a07871 Fix loop offline videos, make loop not reload video 2024-06-13 11:21:48 +02:00
31 changed files with 732 additions and 204 deletions
@@ -104,6 +104,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainTutorial: TutorialFragment;
lateinit var _fragMainPlaylists: PlaylistsFragment;
lateinit var _fragMainPlaylist: PlaylistFragment;
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment;
lateinit var _fragHistory: HistoryFragment;
lateinit var _fragSourceDetail: SourceDetailFragment;
@@ -246,6 +247,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSources = SourcesFragment.newInstance();
_fragMainPlaylists = PlaylistsFragment.newInstance();
_fragMainPlaylist = PlaylistFragment.newInstance();
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
_fragPostDetail = PostDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance();
@@ -331,6 +333,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSources.topBar = _fragTopBarAdd;
_fragMainPlaylists.topBar = _fragTopBarGeneral;
_fragMainPlaylist.topBar = _fragTopBarNavigation;
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
_fragPostDetail.topBar = _fragTopBarNavigation;
_fragWatchlist.topBar = _fragTopBarNavigation;
_fragHistory.topBar = _fragTopBarNavigation;
@@ -1044,6 +1047,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
SourcesFragment::class -> _fragMainSources as T;
PlaylistsFragment::class -> _fragMainPlaylists as T;
PlaylistFragment::class -> _fragMainPlaylist as T;
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
PostDetailFragment::class -> _fragPostDetail as T;
WatchLaterFragment::class -> _fragWatchlist as T;
HistoryFragment::class -> _fragHistory as T;
@@ -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
) {
}
@@ -10,4 +10,6 @@ interface IPlatformContentDetails : IPlatformContent {
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
fun getPlaybackTracker(): IPlaybackTracker?;
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
}
@@ -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;
}
@@ -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 ")
@@ -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)});
@@ -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);
}
}
@@ -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();
@@ -204,7 +204,7 @@ class ChannelFragment : MainFragment() {
}
is IPlatformPlaylist -> {
fragment.navigate<PlaylistFragment>(v)
fragment.navigate<RemotePlaylistFragment>(v)
}
is IPlatformPost -> {
@@ -6,28 +6,32 @@ import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
import com.futo.platformplayer.views.adapters.feedtypes.PreviewNestedVideoViewHolder
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.withTimestamp
import kotlin.math.floor
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
@@ -183,7 +187,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
}
} else if (content is IPlatformPlaylist) {
fragment.navigate<PlaylistFragment>(content);
fragment.navigate<RemotePlaylistFragment>(content);
} else if (content is IPlatformPost) {
fragment.navigate<PostDetailFragment>(content);
}
@@ -194,7 +198,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
StatePlayer.instance.clearQueue();
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
};
ContentType.PLAYLIST -> fragment.navigate<PlaylistFragment>(url);
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url);
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
else -> {};
}
@@ -156,7 +156,7 @@ class ContentSearchResultsFragment : MainFragment() {
onSearch.subscribe(this) {
if(it.isHttpUrl()) {
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
navigate<PlaylistFragment>(it);
navigate<RemotePlaylistFragment>(it);
else if(StatePlatform.instance.hasEnabledChannelClient(it))
navigate<ChannelFragment>(it);
else
@@ -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();
}
}
}
};
}
}
@@ -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);
}
}
}
@@ -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 {}
}
}
@@ -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 {
@@ -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})");
@@ -15,6 +15,7 @@ import com.futo.platformplayer.R
open class InsertedViewAdapterWithLoader<TViewHolder> : InsertedViewAdapter<TViewHolder> where TViewHolder : ViewHolder {
private var _loaderView: ImageView? = null;
private var _loading = false;
val isLoading get() = _loading;
constructor(
context: Context,
@@ -43,7 +43,7 @@ class VideoListEditorViewHolder : ViewHolder {
val onRemove = Event1<IPlatformVideo>();
@SuppressLint("ClickableViewAccessibility")
constructor(view: View, touchHelper: ItemTouchHelper) : super(view) {
constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) {
_root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true;
@@ -59,7 +59,7 @@ class VideoListEditorViewHolder : ViewHolder {
_layoutDownloaded = view.findViewById(R.id.layout_downloaded);
_imageDragDrop.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
if (touchHelper != null && event.action == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this);
}
false
@@ -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>