mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39e7d64d3f | |||
| 35d8610c00 | |||
| bc550ae8f5 | |||
| c76ef7f19b | |||
| b7781264d3 | |||
| 696e03941a | |||
| 4609a351dc | |||
| c275415a49 | |||
| 486ebd6bc8 | |||
| 74b9926647 | |||
| 2a6ba6d541 | |||
| 931216ab7d | |||
| 916936e179 | |||
| b535353365 | |||
| be2ae096ee | |||
| 948b85ddcb | |||
| ae904b4cd8 | |||
| aad50e7b50 | |||
| ff28a07871 | |||
| 414b6e24d2 | |||
| 9499afd815 | |||
| e7aca5cd25 | |||
| 80a6a8ac9f |
+1
-1
@@ -7,7 +7,7 @@ By using the software, you agree to all of the terms and conditions below.
|
||||
FUTO Holdings, Inc. (the “Licensor”) grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below.
|
||||
|
||||
## Limitations
|
||||
You may use or modify the software for only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application.
|
||||
You may use or modify the software only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application.
|
||||
|
||||
You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes.
|
||||
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
<service android:name=".services.DownloadService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name=".services.ExportingService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<receiver android:name=".receivers.MediaControlReceiver" />
|
||||
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
||||
|
||||
@@ -436,7 +436,7 @@ class PlatformPlaylist extends PlatformContent {
|
||||
constructor(obj) {
|
||||
super(obj, 4);
|
||||
this.plugin_type = "PlatformPlaylist";
|
||||
this.videoCount = obj.videoCount ?? 0;
|
||||
this.videoCount = obj.videoCount ?? -1;
|
||||
this.thumbnail = obj.thumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ")
|
||||
|
||||
+1
-1
@@ -14,6 +14,6 @@ open class JSPlaylist : JSContent, IPlatformPlaylist {
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
||||
val contextName = "Playlist";
|
||||
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
|
||||
videoCount = obj.getOrDefault(config, "videoCount", contextName, 0)!!;
|
||||
videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
|
||||
}
|
||||
}
|
||||
+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;
|
||||
|
||||
@@ -1,47 +1,37 @@
|
||||
package com.futo.platformplayer.downloads
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.arthenica.ffmpegkit.*
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.LogCallback
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import com.arthenica.ffmpegkit.StatisticsCallback
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanBitrate
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class VideoExport {
|
||||
var state: State = State.QUEUED;
|
||||
|
||||
var videoLocal: VideoLocal;
|
||||
var videoSource: LocalVideoSource?;
|
||||
var audioSource: LocalAudioSource?;
|
||||
var subtitleSource: LocalSubtitleSource?;
|
||||
|
||||
var progress: Double = 0.0;
|
||||
var isCancelled = false;
|
||||
|
||||
var error: String? = null;
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
val onStateChanged = Event1<State>();
|
||||
@kotlinx.serialization.Transient
|
||||
val onProgressChanged = Event1<Double>();
|
||||
|
||||
fun changeState(newState: State) {
|
||||
state = newState;
|
||||
onStateChanged.emit(newState);
|
||||
}
|
||||
|
||||
constructor(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||
this.videoLocal = videoLocal;
|
||||
this.videoSource = videoSource;
|
||||
@@ -50,8 +40,6 @@ class VideoExport {
|
||||
}
|
||||
|
||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
||||
if(isCancelled) throw CancellationException("Export got cancelled");
|
||||
|
||||
val v = videoSource;
|
||||
val a = audioSource;
|
||||
val s = subtitleSource;
|
||||
@@ -107,7 +95,6 @@ class VideoExport {
|
||||
throw Exception("Cannot export when no audio or video source is set.");
|
||||
}
|
||||
|
||||
onProgressChanged.emit(100.0);
|
||||
return@coroutineScope outputFile;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-13
@@ -8,7 +8,7 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -16,12 +16,13 @@ import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.toHumanBytesSize
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
||||
import com.futo.platformplayer.views.items.ActiveDownloadItem
|
||||
import com.futo.platformplayer.views.items.PlaylistDownloadItem
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -64,16 +65,6 @@ class DownloadsFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
};
|
||||
StateDownloads.instance.onExportsChanged.subscribe(this) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
Logger.i(TAG, "Reloading UI for exports");
|
||||
_view?.reloadUI()
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to reload UI for exports", e)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -81,7 +72,6 @@ class DownloadsFragment : MainFragment() {
|
||||
|
||||
StateDownloads.instance.onDownloadsChanged.remove(this);
|
||||
StateDownloads.instance.onDownloadedChanged.remove(this);
|
||||
StateDownloads.instance.onExportsChanged.remove(this);
|
||||
}
|
||||
|
||||
private class DownloadsView : LinearLayout {
|
||||
|
||||
+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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+45
-88
@@ -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
|
||||
@@ -30,7 +27,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 +66,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 +131,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);
|
||||
@@ -151,58 +145,62 @@ class PlaylistFragment : MainFragment() {
|
||||
}
|
||||
|
||||
fun onShown(parameter: Any?) {
|
||||
_taskLoadPlaylist.cancel();
|
||||
_taskLoadPlaylist.cancel()
|
||||
|
||||
if (parameter is Playlist?) {
|
||||
_playlist = parameter;
|
||||
_remotePlaylist = null;
|
||||
_url = null;
|
||||
_playlist = parameter
|
||||
_url = null
|
||||
|
||||
if(parameter != null) {
|
||||
setName(parameter.name);
|
||||
setVideos(parameter.videos, true);
|
||||
setVideoCount(parameter.videos.size);
|
||||
setButtonDownloadVisible(true);
|
||||
setButtonEditVisible(true);
|
||||
if (parameter != null) {
|
||||
setName(parameter.name)
|
||||
setVideos(parameter.videos, true)
|
||||
setVideoCount(parameter.videos.size)
|
||||
setButtonDownloadVisible(true)
|
||||
setButtonEditVisible(true)
|
||||
|
||||
if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
||||
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||
StatePlaylists.instance.playlistStore.save(parameter)
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
||||
arrayListOf()
|
||||
)
|
||||
UIDialogs.toast("Playlist saved")
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
setName(null);
|
||||
setVideos(null, false);
|
||||
setVideoCount(-1);
|
||||
setButtonDownloadVisible(false);
|
||||
setButtonEditVisible(false);
|
||||
setName(null)
|
||||
setVideos(null, false)
|
||||
setVideoCount(-1)
|
||||
setButtonDownloadVisible(false)
|
||||
setButtonEditVisible(false)
|
||||
}
|
||||
|
||||
//TODO: Do I have to remove the showConvertPlaylistButton(); button here?
|
||||
} else if (parameter is IPlatformPlaylist) {
|
||||
_playlist = null;
|
||||
_remotePlaylist = null;
|
||||
_url = parameter.url;
|
||||
_playlist = null
|
||||
_url = parameter.url
|
||||
|
||||
setVideoCount(parameter.videoCount);
|
||||
setName(parameter.name);
|
||||
setVideos(null, false);
|
||||
setButtonDownloadVisible(false);
|
||||
setButtonEditVisible(false);
|
||||
setVideoCount(parameter.videoCount)
|
||||
setName(parameter.name)
|
||||
setVideos(null, false)
|
||||
setButtonDownloadVisible(false)
|
||||
setButtonEditVisible(false)
|
||||
|
||||
fetchPlaylist();
|
||||
showConvertPlaylistButton();
|
||||
fetchPlaylist()
|
||||
} else if (parameter is String) {
|
||||
_playlist = null;
|
||||
_remotePlaylist = null;
|
||||
_url = parameter;
|
||||
_playlist = null
|
||||
_url = parameter
|
||||
|
||||
setName(null);
|
||||
setVideos(null, false);
|
||||
setVideoCount(-1);
|
||||
setButtonDownloadVisible(false);
|
||||
setButtonEditVisible(false);
|
||||
setName(null)
|
||||
setVideos(null, false)
|
||||
setVideoCount(-1)
|
||||
setButtonDownloadVisible(false)
|
||||
setButtonEditVisible(false)
|
||||
|
||||
fetchPlaylist();
|
||||
showConvertPlaylistButton();
|
||||
fetchPlaylist()
|
||||
}
|
||||
|
||||
_playlist?.let {
|
||||
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
|
||||
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,34 +240,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 +260,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 +284,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
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.StatePlayer
|
||||
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
|
||||
|
||||
enum class Action {
|
||||
PLAY_ALL, SHUFFLE, PLAY, NONE
|
||||
}
|
||||
|
||||
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 {
|
||||
convertPlaylist(false, Action.PLAY, holder.video)
|
||||
}
|
||||
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 {
|
||||
convertPlaylist(false, Action.PLAY_ALL);
|
||||
};
|
||||
buttonShuffle.setOnClickListener {
|
||||
convertPlaylist(false, Action.SHUFFLE);
|
||||
};
|
||||
|
||||
_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 convertPlaylist(
|
||||
savePlaylist: Boolean, action: Action, video: IPlatformVideo? = null
|
||||
) {
|
||||
val remotePlaylist = _remotePlaylist
|
||||
if (remotePlaylist == null) {
|
||||
UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading))
|
||||
return
|
||||
}
|
||||
|
||||
val convert = {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (savePlaylist) {
|
||||
StatePlaylists.instance.playlistStore.save(playlist)
|
||||
}
|
||||
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
UIDialogs.toast("Playlist converted")
|
||||
it.dismiss()
|
||||
_fragment.navigate<PlaylistFragment>(playlist)
|
||||
when (action) {
|
||||
Action.SHUFFLE -> StatePlayer.instance.setPlaylist(
|
||||
playlist, focus = true, shuffle = true
|
||||
)
|
||||
|
||||
Action.PLAY_ALL -> StatePlayer.instance.setPlaylist(
|
||||
playlist, focus = true
|
||||
)
|
||||
|
||||
Action.PLAY -> {
|
||||
StatePlayer.instance.setPlaylist(
|
||||
playlist, _videos.indexOf(video), true
|
||||
)
|
||||
}
|
||||
|
||||
Action.NONE -> {}
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
UIDialogs.appToast("Failed to convert playlist.\n" + ex.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (remotePlaylist.videoCount > 100) {
|
||||
val c = context ?: return
|
||||
UIDialogs.showConfirmationDialog(
|
||||
c, "Conversion to local playlist is required for this action", convert
|
||||
)
|
||||
} else {
|
||||
convert()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConvertPlaylistButton() {
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||
convertPlaylist(true, Action.NONE);
|
||||
}));
|
||||
}
|
||||
|
||||
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}")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
package com.futo.platformplayer.services
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.downloads.VideoExport
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.share
|
||||
import com.futo.platformplayer.states.Announcement
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
class ExportingService : Service() {
|
||||
private val TAG = "ExportingService";
|
||||
|
||||
private val EXPORT_NOTIF_ID = 4;
|
||||
private val EXPORT_NOTIF_TAG = "export";
|
||||
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
|
||||
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
|
||||
|
||||
//Context
|
||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||
private var _notificationManager: NotificationManager? = null;
|
||||
private var _notificationChannel: NotificationChannel? = null;
|
||||
|
||||
private val _client = ManagedHttpClient();
|
||||
|
||||
private var _started = false;
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Logger.i(TAG, "onStartCommand");
|
||||
|
||||
synchronized(this) {
|
||||
if(_started)
|
||||
return START_STICKY;
|
||||
|
||||
if(!FragmentedStorage.isInitialized) {
|
||||
closeExportSession();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
_started = true;
|
||||
}
|
||||
setupNotificationRequirements();
|
||||
|
||||
_callOnStarted?.invoke(this);
|
||||
_instance = this;
|
||||
|
||||
_scope.launch {
|
||||
try {
|
||||
doExporting();
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
try {
|
||||
StateAnnouncement.instance.registerAnnouncementSession(
|
||||
Announcement(
|
||||
"rootExportException",
|
||||
"An root export service exception happened",
|
||||
ex.message ?: "",
|
||||
AnnouncementType.SESSION,
|
||||
OffsetDateTime.now()
|
||||
)
|
||||
);
|
||||
} catch(_: Throwable){}
|
||||
}
|
||||
};
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
fun setupNotificationRequirements() {
|
||||
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||
this.enableVibration(false);
|
||||
this.setSound(null, null);
|
||||
};
|
||||
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
Logger.i(TAG, "onCreate");
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? {
|
||||
return null;
|
||||
}
|
||||
|
||||
private suspend fun doExporting() {
|
||||
Logger.i(TAG, "doExporting - Starting Exports");
|
||||
val ignore = mutableListOf<VideoExport>();
|
||||
var currentExport: VideoExport? = StateDownloads.instance.getExporting().firstOrNull();
|
||||
while (currentExport != null)
|
||||
{
|
||||
try{
|
||||
notifyExport(currentExport);
|
||||
doExport(applicationContext, currentExport);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed export [${currentExport.videoLocal.name}]: ${ex.message}", ex);
|
||||
currentExport.error = ex.message;
|
||||
currentExport.changeState(VideoExport.State.ERROR);
|
||||
ignore.add(currentExport);
|
||||
|
||||
//Give it a sec
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
currentExport = StateDownloads.instance.getExporting().filter { !ignore.contains(it) }.firstOrNull();
|
||||
}
|
||||
Logger.i(TAG, "doExporting - Ending Exports");
|
||||
stopService(this);
|
||||
}
|
||||
|
||||
private suspend fun doExport(context: Context, export: VideoExport) {
|
||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||
|
||||
export.changeState(VideoExport.State.EXPORTING);
|
||||
|
||||
var lastNotifyTime: Long = 0L;
|
||||
val file = export.export(context) { progress ->
|
||||
export.progress = progress;
|
||||
|
||||
val currentTime = System.currentTimeMillis();
|
||||
if (currentTime - lastNotifyTime > 500) {
|
||||
notifyExport(export);
|
||||
lastNotifyTime = currentTime;
|
||||
}
|
||||
}
|
||||
export.changeState(VideoExport.State.COMPLETED);
|
||||
Logger.i(TAG, "Export [${export.videoLocal.name}] finished");
|
||||
StateDownloads.instance.removeExport(export);
|
||||
notifyExport(export);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
|
||||
file.share(this@ExportingService);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyExport(export: VideoExport) {
|
||||
val channel = _notificationChannel ?: return;
|
||||
|
||||
val bringUpIntent = Intent(this, MainActivity::class.java);
|
||||
bringUpIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
bringUpIntent.action = "TAB";
|
||||
bringUpIntent.putExtra("TAB", "Exports");
|
||||
|
||||
var builder = NotificationCompat.Builder(this, EXPORT_NOTIF_TAG)
|
||||
.setSmallIcon(R.drawable.ic_export)
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
|
||||
.setContentTitle("${export.state}: ${export.videoLocal.name}")
|
||||
.setContentText(export.getExportInfo())
|
||||
.setProgress(100, (export.progress * 100).toInt(), export.progress == 0.0)
|
||||
.setChannelId(channel.id)
|
||||
|
||||
val notif = builder.build();
|
||||
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(EXPORT_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||
} else {
|
||||
startForeground(EXPORT_NOTIF_ID, notif);
|
||||
}
|
||||
}
|
||||
|
||||
fun closeExportSession() {
|
||||
Logger.i(TAG, "closeExportSession");
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
_notificationManager?.cancel(EXPORT_NOTIF_ID);
|
||||
stopService();
|
||||
_started = false;
|
||||
super.stopSelf();
|
||||
}
|
||||
override fun onDestroy() {
|
||||
Logger.i(TAG, "onDestroy");
|
||||
_instance = null;
|
||||
_scope.cancel("onDestroy");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var _instance: ExportingService? = null;
|
||||
private var _callOnStarted: ((ExportingService)->Unit)? = null;
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateService(context: Context, handle: ((ExportingService)->Unit)? = null) {
|
||||
if(!FragmentedStorage.isInitialized)
|
||||
return;
|
||||
if(_instance == null) {
|
||||
_callOnStarted = handle;
|
||||
val intent = Intent(context, ExportingService::class.java);
|
||||
context.startForegroundService(intent);
|
||||
}
|
||||
else _instance?.let {
|
||||
if(handle != null)
|
||||
handle(it);
|
||||
}
|
||||
}
|
||||
@Synchronized
|
||||
fun getService() : ExportingService? {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopService(service: ExportingService? = null) {
|
||||
(service ?: _instance)?.let {
|
||||
if(_instance == it)
|
||||
_instance = null;
|
||||
it.closeExportSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,9 +445,6 @@ class StateApp {
|
||||
DownloadService.getOrCreateService(context);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Check [Exports]");
|
||||
StateDownloads.instance.checkForExportTodos();
|
||||
|
||||
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
|
||||
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
|
||||
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.StatFs
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
@@ -27,10 +27,14 @@ import com.futo.platformplayer.models.DiskUsage
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.models.PlaylistDownloaded
|
||||
import com.futo.platformplayer.services.DownloadService
|
||||
import com.futo.platformplayer.services.ExportingService
|
||||
import com.futo.platformplayer.share
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
/***
|
||||
* Used to maintain downloads
|
||||
@@ -50,12 +54,8 @@ class StateDownloads {
|
||||
private val _downloadPlaylists = FragmentedStorage.storeJson<PlaylistDownloadDescriptor>("playlistDownloads")
|
||||
.load();
|
||||
|
||||
private val _exporting = FragmentedStorage.storeJson<VideoExport>("exporting")
|
||||
.load();
|
||||
|
||||
private lateinit var _downloadedSet: HashSet<PlatformID>;
|
||||
|
||||
val onExportsChanged = Event0();
|
||||
val onDownloadsChanged = Event0();
|
||||
val onDownloadedChanged = Event0();
|
||||
|
||||
@@ -457,17 +457,6 @@ class StateDownloads {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
|
||||
val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
|
||||
for (export in exporting)
|
||||
_exporting.delete(export);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to delete dangling export:", ex);
|
||||
UIDialogs.toast("Failed to delete dangling export:\n" + ex);
|
||||
}
|
||||
|
||||
return Pair(totalDeletedCount, totalDeleted);
|
||||
}
|
||||
|
||||
@@ -475,66 +464,41 @@ class StateDownloads {
|
||||
return _downloadsDirectory;
|
||||
}
|
||||
|
||||
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
UIDialogs.showDialogProgress(context) {
|
||||
it.setText("Exporting content..");
|
||||
it.setProgress(0f);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val export = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
|
||||
try {
|
||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||
|
||||
//Export
|
||||
fun getExporting(): List<VideoExport> {
|
||||
return _exporting.getItems();
|
||||
}
|
||||
fun checkForExportTodos() {
|
||||
if(_exporting.hasItems()) {
|
||||
StateApp.withContext {
|
||||
ExportingService.getOrCreateService(it);
|
||||
val file = export.export(context) { progress ->
|
||||
val now = System.currentTimeMillis();
|
||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||
it.setProgress(progress);
|
||||
lastNotifyTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(100.0f)
|
||||
it.dismiss()
|
||||
|
||||
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
|
||||
file.share(context);
|
||||
};
|
||||
}
|
||||
} catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed export [${export.videoLocal.name}]: ${ex.message}", ex);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun validateExport(export: VideoExport) {
|
||||
if(_exporting.hasItem { it.videoLocal.url == export.videoLocal.url })
|
||||
throw AlreadyQueuedException("Video [${export.videoLocal.name}] is already queued for export");
|
||||
}
|
||||
fun export(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, notify: Boolean = true) {
|
||||
val shortName = if(videoLocal.name.length > 23)
|
||||
videoLocal.name.substring(0, 20) + "...";
|
||||
else
|
||||
videoLocal.name;
|
||||
|
||||
val videoExport = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
|
||||
|
||||
try {
|
||||
validateExport(videoExport);
|
||||
_exporting.save(videoExport);
|
||||
|
||||
if(notify) {
|
||||
UIDialogs.toast("Exporting [${shortName}]");
|
||||
StateApp.withContext { ExportingService.getOrCreateService(it) };
|
||||
onExportsChanged.emit();
|
||||
}
|
||||
}
|
||||
catch (ex: AlreadyQueuedException) {
|
||||
Logger.e(TAG, "File is already queued for export.", ex);
|
||||
StateApp.withContext { ExportingService.getOrCreateService(it) };
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
StateApp.withContext {
|
||||
UIDialogs.showDialog(
|
||||
it,
|
||||
R.drawable.ic_error,
|
||||
"Failed to start export due to:\n${ex.message}", null, null,
|
||||
0,
|
||||
UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeExport(export: VideoExport) {
|
||||
_exporting.delete(export);
|
||||
export.isCancelled = true;
|
||||
onExportsChanged.emit();
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "StateDownloads";
|
||||
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
@@ -49,6 +48,7 @@ class MonetizationView : LinearLayout {
|
||||
|
||||
private val _taskLoadMerchandise = TaskHandler<String, List<StoreItem>>(StateApp.instance.scopeGetter, { url ->
|
||||
val client = ManagedHttpClient();
|
||||
Logger.i(TAG, "Loading https://storecache.grayjay.app/StoreData?url=$url")
|
||||
val result = client.get("https://storecache.grayjay.app/StoreData?url=$url")
|
||||
if (!result.isOk) {
|
||||
throw Exception("Failed to retrieve store data.");
|
||||
|
||||
+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,
|
||||
|
||||
@@ -33,6 +33,7 @@ open class PlaylistView : LinearLayout {
|
||||
protected val _platformIndicator: PlatformIndicator;
|
||||
protected val _textPlaylistName: TextView
|
||||
protected val _textVideoCount: TextView
|
||||
protected val _textVideoCountLabel: TextView;
|
||||
protected val _textPlaylistItems: TextView
|
||||
protected val _textChannelName: TextView
|
||||
protected var _neopassAnimator: ObjectAnimator? = null;
|
||||
@@ -62,6 +63,7 @@ open class PlaylistView : LinearLayout {
|
||||
_platformIndicator = findViewById(R.id.thumbnail_platform);
|
||||
_textPlaylistName = findViewById(R.id.text_playlist_name);
|
||||
_textVideoCount = findViewById(R.id.text_video_count);
|
||||
_textVideoCountLabel = findViewById(R.id.text_video_count_label);
|
||||
_textChannelName = findViewById(R.id.text_channel_name);
|
||||
_textPlaylistItems = findViewById(R.id.text_playlist_items);
|
||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||
@@ -137,7 +139,15 @@ open class PlaylistView : LinearLayout {
|
||||
.crossfade()
|
||||
.into(_imageThumbnail);
|
||||
|
||||
_textVideoCount.text = content.videoCount.toString();
|
||||
if(content.videoCount >= 0) {
|
||||
_textVideoCount.text = content.videoCount.toString();
|
||||
_textVideoCount.visibility = View.VISIBLE;
|
||||
_textVideoCountLabel.visibility = VISIBLE;
|
||||
}
|
||||
else {
|
||||
_textVideoCount.visibility = View.GONE;
|
||||
_textVideoCountLabel.visibility = GONE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
currentPlaylist = null;
|
||||
|
||||
+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
|
||||
|
||||
+10
-3
@@ -1,12 +1,14 @@
|
||||
package com.futo.platformplayer.views.adapters.viewholders
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
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 +47,15 @@ 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);
|
||||
if(value.playlist.videoCount >= 0) {
|
||||
_textMetadata.text = "${value.playlist.videoCount} " + _view.context.getString(R.string.videos);
|
||||
_textMetadata.visibility = View.VISIBLE;
|
||||
}
|
||||
else
|
||||
_textMetadata.visibility = View.GONE;
|
||||
_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 +69,6 @@ class ImportPlaylistsViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||
}
|
||||
|
||||
class SelectablePlaylist(
|
||||
val playlist: Playlist,
|
||||
val playlist: IPlatformPlaylistDetails,
|
||||
var selected: Boolean = false
|
||||
) { }
|
||||
+8
-2
@@ -16,6 +16,8 @@ import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<VideoLocal>(
|
||||
@@ -57,10 +59,14 @@ class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<
|
||||
return@changeExternalDownloadDirectory;
|
||||
}
|
||||
|
||||
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
|
||||
}
|
||||
};
|
||||
} else {
|
||||
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -68,6 +68,7 @@
|
||||
android:textColor="@color/gray_7f"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_count_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="8dp"
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
android:fontFamily="@font/inter_light"
|
||||
tools:text="100"
|
||||
android:textColor="@color/gray_7f"
|
||||
app:layout_constraintRight_toLeftOf="@id/text_videos"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_videos" />
|
||||
app:layout_constraintRight_toLeftOf="@id/text_video_count_label"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_video_count_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_playlist"
|
||||
@@ -80,10 +80,10 @@
|
||||
android:textColor="@color/gray_e0"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_videos"/>
|
||||
app:layout_constraintBottom_toTopOf="@id/text_video_count_label"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_videos"
|
||||
android:id="@+id/text_video_count_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12dp"
|
||||
|
||||
Submodule app/src/stable/assets/sources/spotify updated: 843cf2dc4b...4e826dcb6a
Submodule app/src/stable/assets/sources/twitch updated: 8d978dd7bd...b4696e4e2e
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/twitch updated: 8d978dd7bd...b4696e4e2e
Submodule app/src/unstable/assets/sources/youtube updated: c23302da76...5032e4e10a
@@ -2,12 +2,12 @@
|
||||
Package http is the main way for a plugin to make web requests, and is likely a package you will always need.
|
||||
It offers several ways to make web requests as well as websocket connections.
|
||||
|
||||
Before you can use http you need to register it in your plugin config. See [Packages](_blank).
|
||||
Before you can use http you need to register it in your plugin config. See [Packages](/app/src/main/java/com/futo/platformplayer/engine/packages).
|
||||
|
||||
## Basic Info
|
||||
Underneath the http package by default exist two web clients. An authenticated client and a unauthenticated client.
|
||||
The authenticated client has will apply headers and cookies if the user is logged in with your plugin.
|
||||
See [Plugin Authentication](_blank).
|
||||
See [Plugin Authentication](/docs/Authentication.md).
|
||||
These two clients are always available even when the user is not logged in, meaning it behaves similar to the unauthenticated client and can safely use it either way.
|
||||
|
||||
>:warning: **Requests are synchronous**
|
||||
|
||||
Reference in New Issue
Block a user