mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-20 23:05:20 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39e7d64d3f | |||
| 35d8610c00 | |||
| bc550ae8f5 | |||
| c76ef7f19b | |||
| b7781264d3 | |||
| 696e03941a | |||
| 4609a351dc | |||
| c275415a49 | |||
| 486ebd6bc8 | |||
| 74b9926647 | |||
| 2a6ba6d541 | |||
| 931216ab7d | |||
| 916936e179 | |||
| b535353365 | |||
| be2ae096ee | |||
| 948b85ddcb | |||
| ae904b4cd8 | |||
| aad50e7b50 | |||
| ff28a07871 |
@@ -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/youtube updated: c23302da76...5032e4e10a
Submodule app/src/unstable/assets/sources/spotify updated: 843cf2dc4b...4e826dcb6a
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