mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 959c192762 | |||
| 8be7b1272b | |||
| 6b57878275 | |||
| 66c7741c38 | |||
| b370af9d91 | |||
| 40b86cb5de | |||
| 84622e22aa | |||
| 092b20041e | |||
| f6cc00f471 |
@@ -402,6 +402,11 @@
|
||||
<div class="code">
|
||||
{{req.code}}
|
||||
</div>
|
||||
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
|
||||
<a :href="req.docUrl" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="parameter" v-for="parameter in req.parameters">
|
||||
<div class="name">
|
||||
@@ -538,6 +543,7 @@
|
||||
<!--<script src="./dependencies/vue.js"></script>-->
|
||||
<!--<script src="./dependencies/vuetify.js"></script>-->
|
||||
<script src="./source_docs.js"></script>
|
||||
<script src="./source_doc_urls.js"></script>
|
||||
<script src="./source.js"></script>
|
||||
<script src="./dev_bridge.js"></script>
|
||||
<script>
|
||||
@@ -574,6 +580,9 @@
|
||||
Testing: {
|
||||
requests: sourceDocs.map(x=>{
|
||||
x.parameters.forEach(y=>y.value = null);
|
||||
|
||||
if(sourceDocUrls[x.title])
|
||||
x.docUrl = sourceDocUrls[x.title];
|
||||
return x;
|
||||
}),
|
||||
lastResult: "",
|
||||
|
||||
@@ -311,7 +311,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
||||
var alwaysReloadFromCache: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
|
||||
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
|
||||
var peekChannelContents: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||
fun clearChannelCache() {
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||
StateCache.instance.clear();
|
||||
|
||||
@@ -510,10 +510,15 @@ class UISlideOverlays {
|
||||
}
|
||||
}
|
||||
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
||||
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
||||
showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
|
||||
StateDownloads.instance.download(playlist, px, bitrate);
|
||||
};
|
||||
}
|
||||
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
|
||||
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
|
||||
StateDownloads.instance.downloadWatchLater(px, bitrate);
|
||||
})
|
||||
}
|
||||
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
||||
val items = arrayListOf<View>();
|
||||
var menu: SlideUpMenuOverlay? = null;
|
||||
@@ -783,8 +788,8 @@ class UISlideOverlays {
|
||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
||||
}
|
||||
|
||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
|
||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
|
||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
|
||||
overlay.show();
|
||||
return overlay;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ class CachedPlatformClient : IPlatformClient {
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.getChannelContents(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);
|
||||
|
||||
@@ -84,6 +84,15 @@ interface IPlatformClient {
|
||||
*/
|
||||
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
||||
|
||||
/**
|
||||
* Describes what the plugin is capable on peek channel results
|
||||
*/
|
||||
fun getPeekChannelTypes(): List<String>;
|
||||
/**
|
||||
* Peeks contents of a channel, upload time descending
|
||||
*/
|
||||
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
|
||||
|
||||
/**
|
||||
* Gets the channel url associated with a claimType
|
||||
*/
|
||||
|
||||
@@ -13,10 +13,12 @@ data class PlatformClientCapabilities(
|
||||
val hasGetChannelUrlByClaim: Boolean = false,
|
||||
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
||||
val hasGetSearchCapabilities: Boolean = false,
|
||||
val hasGetSearchChannelContentsCapabilities: Boolean = false,
|
||||
val hasGetChannelCapabilities: Boolean = false,
|
||||
val hasGetLiveEvents: Boolean = false,
|
||||
val hasGetLiveChatWindow: Boolean = false,
|
||||
val hasGetContentChapters: Boolean = false
|
||||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ open class PlatformAuthorLink {
|
||||
val id: PlatformID;
|
||||
val name: String;
|
||||
val url: String;
|
||||
val thumbnail: String?;
|
||||
var thumbnail: String?;
|
||||
var subscribers: Long? = null; //Optional
|
||||
|
||||
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||
@@ -58,6 +59,7 @@ import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.reflect.full.findAnnotations
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
import kotlin.streams.asSequence
|
||||
|
||||
open class JSClient : IPlatformClient {
|
||||
val config: SourcePluginConfig;
|
||||
@@ -73,6 +75,7 @@ open class JSClient : IPlatformClient {
|
||||
private var _searchCapabilities: ResultCapabilities? = null;
|
||||
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
||||
private var _channelCapabilities: ResultCapabilities? = null;
|
||||
private var _peekChannelTypes: List<String>? = null;
|
||||
|
||||
protected val _script: String;
|
||||
|
||||
@@ -91,7 +94,11 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
private val _busyLock = Object();
|
||||
private var _busyCounter = 0;
|
||||
private var _busyAction = "";
|
||||
val isBusy: Boolean get() = _busyCounter > 0;
|
||||
val isBusyAction: String get() {
|
||||
return _busyAction;
|
||||
}
|
||||
|
||||
val settings: HashMap<String, String?> get() = descriptor.settings;
|
||||
|
||||
@@ -150,6 +157,8 @@ open class JSClient : IPlatformClient {
|
||||
if(it is ScriptCaptchaRequiredException)
|
||||
onCaptchaException.emit(this, it);
|
||||
};
|
||||
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||
this._context = context;
|
||||
@@ -173,6 +182,8 @@ open class JSClient : IPlatformClient {
|
||||
if(it is ScriptCaptchaRequiredException)
|
||||
onCaptchaException.emit(this, it);
|
||||
};
|
||||
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
|
||||
open fun getCopy(): JSClient {
|
||||
@@ -214,9 +225,11 @@ open class JSClient : IPlatformClient {
|
||||
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
|
||||
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
|
||||
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
||||
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
|
||||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -260,7 +273,7 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
|
||||
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.getHome()"));
|
||||
@@ -268,7 +281,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith {
|
||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
||||
.toArray()
|
||||
@@ -298,7 +311,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||
@@ -306,6 +319,9 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
|
||||
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
|
||||
if(!capabilities.hasGetSearchChannelContentsCapabilities)
|
||||
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
|
||||
|
||||
ensureEnabled();
|
||||
if (_searchChannelContentsCapabilities != null)
|
||||
return _searchChannelContentsCapabilities!!;
|
||||
@@ -319,7 +335,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
|
||||
ensureEnabled();
|
||||
if(!capabilities.hasSearchChannelContents)
|
||||
throw IllegalStateException("This plugin does not support channel search");
|
||||
@@ -331,7 +347,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSOptional
|
||||
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
||||
@JSDocsParameter("query", "Query that channels should match")
|
||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
|
||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSChannelPager(config, this,
|
||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||
@@ -351,7 +367,7 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith {
|
||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSChannel(config,
|
||||
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
||||
@@ -378,12 +394,46 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||
}
|
||||
|
||||
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
|
||||
override fun getPeekChannelTypes(): List<String> {
|
||||
if(!capabilities.hasPeekChannelContents)
|
||||
return listOf();
|
||||
try {
|
||||
if (_peekChannelTypes != null) {
|
||||
return _peekChannelTypes!!;
|
||||
}
|
||||
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
|
||||
|
||||
_peekChannelTypes = arr.keys.mapNotNull {
|
||||
val str = arr.get<V8ValueString>(it);
|
||||
return@mapNotNull str.value;
|
||||
};
|
||||
return _peekChannelTypes ?: listOf();
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("getPeekChannelTypes", ex);
|
||||
return listOf();
|
||||
}
|
||||
}
|
||||
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
|
||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
|
||||
ensureEnabled();
|
||||
|
||||
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
|
||||
return@isBusyWith items.keys.mapNotNull {
|
||||
val obj = items.get<V8ValueObject>(it);
|
||||
return@mapNotNull IJSContent.fromV8(this, obj);
|
||||
};
|
||||
}
|
||||
|
||||
@JSOptional
|
||||
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
||||
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
||||
@@ -444,7 +494,7 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith IJSContentDetails.fromV8(this,
|
||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||
@@ -453,7 +503,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSOptional //getContentChapters = function(url, initialData)
|
||||
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
|
||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
|
||||
if(!capabilities.hasGetContentChapters)
|
||||
return@isBusyWith listOf();
|
||||
ensureEnabled();
|
||||
@@ -464,7 +514,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSOptional
|
||||
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith {
|
||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
|
||||
if(!capabilities.hasGetPlaybackTracker)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
@@ -478,7 +528,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
|
||||
ensureEnabled();
|
||||
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
||||
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||
@@ -496,7 +546,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||
@JSDocsParameter("url", "Url of live stream")
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith {
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||
if(!capabilities.hasGetLiveChatWindow)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
@@ -505,7 +555,7 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||
@JSDocsParameter("url", "Url of live stream")
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith {
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||
if(!capabilities.hasGetLiveEvents)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
@@ -518,7 +568,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
|
||||
ensureEnabled();
|
||||
if(!capabilities.hasSearchPlaylists)
|
||||
throw IllegalStateException("This plugin does not support playlist search");
|
||||
@@ -528,15 +578,22 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
||||
@JSDocsParameter("url", "Url of playlist")
|
||||
override fun isPlaylistUrl(url: String): Boolean {
|
||||
ensureEnabled();
|
||||
if (!capabilities.hasGetPlaylist)
|
||||
return false;
|
||||
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
|
||||
|
||||
try {
|
||||
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||
.value;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("isPlaylistUrl", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@JSOptional
|
||||
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
||||
@JSDocsParameter("url", "Url of playlist")
|
||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
|
||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
||||
}
|
||||
@@ -633,19 +690,24 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
|
||||
|
||||
private fun <T> isBusyWith(handle: ()->T): T {
|
||||
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
||||
try {
|
||||
synchronized(_busyLock) {
|
||||
_busyCounter++;
|
||||
}
|
||||
_busyAction = actionName;
|
||||
return handle();
|
||||
}
|
||||
finally {
|
||||
_busyAction = "";
|
||||
synchronized(_busyLock) {
|
||||
_busyCounter--;
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun <T> isBusyWith(handle: ()->T): T {
|
||||
return isBusyWith("Unknown", handle);
|
||||
}
|
||||
|
||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||
if(ex is PluginEngineException)
|
||||
@@ -662,10 +724,43 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
companion object {
|
||||
val TAG = "JSClient";
|
||||
private val _lock = Object();
|
||||
private var _docs: Map<String, String>? = null;
|
||||
|
||||
fun getMethodDocs(names: List<String>): Map<String, String>? {
|
||||
synchronized(_lock) {
|
||||
if(_docs == null) {
|
||||
val client = ManagedHttpClient();
|
||||
val docs = names
|
||||
.map { stringWithoutBrackets(it) }
|
||||
.distinct()
|
||||
.parallelStream()
|
||||
.map {
|
||||
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
|
||||
val resp = client.head(url);
|
||||
if(resp.isOk)
|
||||
return@map Pair(it, url);
|
||||
else
|
||||
return@map null;
|
||||
}.asSequence()
|
||||
.filterNotNull()
|
||||
.toMap();
|
||||
_docs = docs;
|
||||
}
|
||||
return _docs;
|
||||
}
|
||||
}
|
||||
fun getMethodDocUrls(): Map<String, String>? {
|
||||
if(_docs != null)
|
||||
return _docs;
|
||||
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||
return getMethodDocs(methods.map { it.name });
|
||||
}
|
||||
|
||||
fun getJSDocs(): List<JSCallDocs> {
|
||||
val docs = mutableListOf<JSCallDocs>();
|
||||
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||
|
||||
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
|
||||
val doc = method.getAnnotation(JSDocs::class.java);
|
||||
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
|
||||
@@ -678,5 +773,12 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
return docs;
|
||||
}
|
||||
|
||||
private fun stringWithoutBrackets(name: String): String {
|
||||
val index = name.indexOf('(');
|
||||
if(index >= 0)
|
||||
return name.substring(0, index);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -45,7 +45,8 @@ class SourcePluginConfig(
|
||||
var enableInSearch: Boolean = true,
|
||||
var enableInHome: Boolean = true,
|
||||
var supportedClaimTypes: List<Int> = listOf(),
|
||||
var primaryClaimFieldType: Int? = null
|
||||
var primaryClaimFieldType: Int? = null,
|
||||
var developerSubmitUrl: String? = null
|
||||
) : IV8PluginConfig {
|
||||
|
||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||
|
||||
+7
-1
@@ -8,6 +8,7 @@ import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -90,7 +91,7 @@ class SourcePluginDescriptor {
|
||||
@Serializable
|
||||
class AppPluginSettings {
|
||||
|
||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 1)
|
||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
|
||||
var checkForUpdates: Boolean = true;
|
||||
|
||||
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||
@@ -130,6 +131,11 @@ class SourcePluginDescriptor {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
|
||||
var allowDeveloperSubmit: Boolean = false;
|
||||
|
||||
|
||||
fun loadDefaults(config: SourcePluginConfig) {
|
||||
if(tabEnabled.enableHome == null)
|
||||
tabEnabled.enableHome = config.enableInHome
|
||||
|
||||
@@ -14,6 +14,6 @@ annotation class JSOptional()
|
||||
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false);
|
||||
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false, val docsUrl: String? = null);
|
||||
@kotlinx.serialization.Serializable
|
||||
data class JSParameterDocs(val name: String, val description: String);
|
||||
@@ -163,24 +163,25 @@ class AirPlayCastingDevice : CastingDevice {
|
||||
}
|
||||
|
||||
connectionState = CastConnectionState.CONNECTED;
|
||||
delay(1000);
|
||||
|
||||
val progressIndex = progressInfo.lowercase().indexOf("position: ");
|
||||
if (progressIndex == -1) {
|
||||
delay(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
|
||||
setTime(progress);
|
||||
|
||||
|
||||
val durationIndex = progressInfo.lowercase().indexOf("duration: ");
|
||||
if (durationIndex == -1) {
|
||||
delay(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
|
||||
setDuration(duration);
|
||||
delay(1000);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
private var _socket: SSLSocket? = null;
|
||||
private var _outputStream: DataOutputStream? = null;
|
||||
private var _outputStreamLock = Object();
|
||||
private var _inputStream: DataInputStream? = null;
|
||||
private var _inputStreamLock = Object();
|
||||
private var _scopeIO: CoroutineScope? = null;
|
||||
private var _requestId = 1;
|
||||
private var _started: Boolean = false;
|
||||
@@ -383,39 +385,44 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
getStatus();
|
||||
|
||||
val buffer = ByteArray(4096);
|
||||
val buffer = ByteArray(409600);
|
||||
|
||||
Logger.i(TAG, "Started receiving.");
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val inputStream = _inputStream ?: break;
|
||||
Log.d(TAG, "Receiving next packet...");
|
||||
val b1 = inputStream.readUnsignedByte();
|
||||
val b2 = inputStream.readUnsignedByte();
|
||||
val b3 = inputStream.readUnsignedByte();
|
||||
val b4 = inputStream.readUnsignedByte();
|
||||
val size = ((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
|
||||
if (size > buffer.size) {
|
||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||
inputStream.skip(size.toLong());
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||
inputStream.read(buffer, 0, size);
|
||||
synchronized(_inputStreamLock)
|
||||
{
|
||||
Log.d(TAG, "Receiving next packet...");
|
||||
val b1 = inputStream.readUnsignedByte();
|
||||
val b2 = inputStream.readUnsignedByte();
|
||||
val b3 = inputStream.readUnsignedByte();
|
||||
val b4 = inputStream.readUnsignedByte();
|
||||
val size =
|
||||
((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
|
||||
if (size > buffer.size) {
|
||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||
inputStream.skip(size.toLong());
|
||||
return@synchronized
|
||||
}
|
||||
|
||||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||
Logger.i(TAG, "Received message: $message");
|
||||
}
|
||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||
inputStream.read(buffer, 0, size);
|
||||
|
||||
try {
|
||||
handleMessage(message);
|
||||
} catch (e:Throwable) {
|
||||
Logger.w(TAG, "Failed to handle message.", e);
|
||||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||
Logger.i(TAG, "Received message: $message");
|
||||
}
|
||||
|
||||
try {
|
||||
handleMessage(message);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to handle message.", e);
|
||||
}
|
||||
}
|
||||
} catch (e: java.net.SocketException) {
|
||||
Logger.e(TAG, "Socket exception while receiving.", e);
|
||||
@@ -588,13 +595,16 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
return;
|
||||
}
|
||||
|
||||
val serializedSizeBE = ByteArray(4);
|
||||
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
|
||||
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
|
||||
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
|
||||
serializedSizeBE[3] = (data.size and 0xff).toByte();
|
||||
outputStream.write(serializedSizeBE);
|
||||
outputStream.write(data);
|
||||
synchronized(_outputStreamLock)
|
||||
{
|
||||
val serializedSizeBE = ByteArray(4);
|
||||
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
|
||||
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
|
||||
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
|
||||
serializedSizeBE[3] = (data.size and 0xff).toByte();
|
||||
outputStream.write(serializedSizeBE);
|
||||
outputStream.write(data);
|
||||
}
|
||||
|
||||
//Log.d(TAG, "Sent ${data.size} bytes.");
|
||||
}
|
||||
|
||||
@@ -242,6 +242,7 @@ class StateCasting {
|
||||
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
||||
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
||||
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
||||
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
||||
|
||||
@@ -104,6 +104,17 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
@HttpGET("/source_docs.js", "application/javascript")
|
||||
val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson";
|
||||
|
||||
@HttpGET("/source_doc_urls.json", "application/json")
|
||||
fun devSourceDocUrlsJson(httpContext: HttpContext) {;
|
||||
val docs = JSClient.getMethodDocUrls();
|
||||
httpContext.respondCode(200, Json.encodeToString(docs), "application/json");
|
||||
}
|
||||
@HttpGET("/source_doc_urls.js", "application/javascript")
|
||||
fun devSourceDocUrlsJs(httpContext: HttpContext) {;
|
||||
val docs = JSClient.getMethodDocUrls();
|
||||
httpContext.respondCode(200, "const sourceDocUrls = " + Json.encodeToString(docs), "application/javascript");
|
||||
}
|
||||
|
||||
//Dependencies
|
||||
//@HttpGET("/dependencies/vue.js", "application/javascript")
|
||||
//val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true);
|
||||
|
||||
@@ -755,6 +755,7 @@ class VideoDownload {
|
||||
companion object {
|
||||
const val TAG = "VideoDownload";
|
||||
const val GROUP_PLAYLIST = "Playlist";
|
||||
const val GROUP_WATCHLATER= "WatchLater";
|
||||
|
||||
fun videoContainerToExtension(container: String): String? {
|
||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -45,7 +44,6 @@ class V8Plugin {
|
||||
private val _clientAuth: ManagedHttpClient;
|
||||
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
||||
|
||||
|
||||
val httpClient: ManagedHttpClient get() = _client;
|
||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
|
||||
@@ -71,6 +69,11 @@ class V8Plugin {
|
||||
private var _busyCounter = 0;
|
||||
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
||||
|
||||
var allowDevSubmit: Boolean = false
|
||||
private set(value) {
|
||||
field = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a busy counter is about to be removed.
|
||||
* Is primarily used to prevent additional calls to dead runtimes.
|
||||
@@ -92,6 +95,10 @@ class V8Plugin {
|
||||
withDependency(getPackage(pack));
|
||||
}
|
||||
|
||||
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||
allowDevSubmit = isAllowed;
|
||||
}
|
||||
|
||||
fun withDependency(context: Context, assetPath: String) : V8Plugin {
|
||||
if(!_deps.containsKey(assetPath))
|
||||
_deps.put(assetPath, getAssetFile(context, assetPath));
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
@@ -12,6 +13,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class PackageBridge : V8Package {
|
||||
@Transient
|
||||
@@ -21,6 +25,7 @@ class PackageBridge : V8Package {
|
||||
@Transient
|
||||
private val _clientAuth: ManagedHttpClient
|
||||
|
||||
|
||||
override val name: String get() = "Bridge";
|
||||
override val variableName: String get() = "bridge";
|
||||
|
||||
@@ -47,6 +52,44 @@ class PackageBridge : V8Package {
|
||||
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", str ?: "null");
|
||||
}
|
||||
|
||||
private val _jsonSerializer = Json { this.prettyPrintIndent = " "; this.prettyPrint = true; };
|
||||
private var _devSubmitClient: ManagedHttpClient? = null;
|
||||
@V8Function
|
||||
fun devSubmit(label: String, data: String) {
|
||||
if(_plugin.config !is SourcePluginConfig)
|
||||
return;
|
||||
if(!_plugin.allowDevSubmit)
|
||||
return;
|
||||
val devUrl = _plugin.config.developerSubmitUrl ?: return;
|
||||
if(_devSubmitClient == null)
|
||||
_devSubmitClient = ManagedHttpClient();
|
||||
|
||||
val stackTrace = Thread.currentThread().stackTrace;
|
||||
val callerMethod = stackTrace.findLast {
|
||||
it.className == JSClient::class.java.name
|
||||
}?.methodName ?: "";
|
||||
val session = StateApp.instance.sessionId;
|
||||
val pluginId = _plugin.config.id;
|
||||
val pluginVersion = _plugin.config.version;
|
||||
|
||||
val obj = DevSubmitData(pluginId, pluginVersion, callerMethod, session, label, data);
|
||||
|
||||
UIDialogs.toast("DevSubmit [${callerMethod}] (${_plugin.config.name})", false);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val json = _jsonSerializer.encodeToString(obj);
|
||||
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl}\n" + json);
|
||||
val resp = _devSubmitClient?.post(devUrl, json, mutableMapOf(Pair("Content-Type", "application/json")));
|
||||
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl} Status: " + (resp?.code?.toString() ?: "-1"))
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
Logger.e(TAG, "DevSubmission to [${devUrl}] failed due to:\n" + ex.message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Serializable
|
||||
class DevSubmitData(val pluginId: String, val pluginVersion: Int, val caller: String, val session: String, val label: String, val data: String)
|
||||
|
||||
@V8Function
|
||||
fun throwTest(str: String) {
|
||||
throw IllegalStateException(str);
|
||||
|
||||
@@ -4,8 +4,11 @@ import android.util.Base64
|
||||
import com.caoccao.javet.annotations.V8Function
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.google.common.hash.Hashing.md5
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
class PackageUtilities : V8Package {
|
||||
@Transient
|
||||
private val _config: IV8PluginConfig;
|
||||
@@ -19,7 +22,31 @@ class PackageUtilities : V8Package {
|
||||
|
||||
@V8Function
|
||||
fun toBase64(arr: ByteArray): String {
|
||||
return Base64.encodeToString(arr, Base64.NO_WRAP);
|
||||
return Base64.encodeToString(arr, Base64.NO_PADDING or Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun fromBase64(str: String): ByteArray {
|
||||
return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun md5(arr: ByteArray): ByteArray {
|
||||
return MessageDigest.getInstance("MD5").digest(arr);
|
||||
}
|
||||
@V8Function
|
||||
fun md5String(str: String): String {
|
||||
return md5(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
|
||||
}
|
||||
|
||||
|
||||
@V8Function
|
||||
fun sha256(arr: ByteArray): ByteArray {
|
||||
return MessageDigest.getInstance("SHA-256").digest(arr);
|
||||
}
|
||||
@V8Function
|
||||
fun sha256String(str: String): String {
|
||||
return sha256(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
|
||||
}
|
||||
|
||||
@V8Function
|
||||
|
||||
+6
-3
@@ -60,8 +60,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformContent>();
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||
val onLongPress = Event1<IPlatformContent>();
|
||||
|
||||
|
||||
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
|
||||
Logger.i(TAG, "getContentPager");
|
||||
|
||||
@@ -103,9 +105,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
}).success {
|
||||
setLoading(false);
|
||||
val posBefore = _results.size;
|
||||
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
||||
_results.addAll(toAdd);
|
||||
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
|
||||
//val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
||||
_results.addAll(it);
|
||||
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), it.size); };
|
||||
}.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load next page.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
|
||||
@@ -157,6 +159,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
||||
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
||||
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
||||
this.onAddToWatchLaterClicked.subscribe(this@ChannelContentsFragment.onAddToWatchLaterClicked::emit);
|
||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -26,6 +26,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
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.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
|
||||
@@ -206,6 +207,12 @@ class ChannelFragment : MainFragment() {
|
||||
StatePlayer.instance.addToQueue(content);
|
||||
}
|
||||
}
|
||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||
if(content is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content));
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]");
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
fragment.navigate<BrowserFragment>(url);
|
||||
}
|
||||
@@ -264,7 +271,7 @@ class ChannelFragment : MainFragment() {
|
||||
_taskLoadPolycentricProfile.cancel();
|
||||
_selectedTabIndex = -1;
|
||||
|
||||
if (!isBack) {
|
||||
if (!isBack || _url == null) {
|
||||
_imageBanner.setImageDrawable(null);
|
||||
|
||||
if (parameter is String) {
|
||||
|
||||
+16
@@ -1,7 +1,9 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Browser
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -118,6 +120,7 @@ class CommentsFragment : MainFragment() {
|
||||
holder.onDelete.subscribe(::onDelete);
|
||||
holder.onRepliesClick.subscribe(::onRepliesClick);
|
||||
holder.onClick.subscribe(::onClick);
|
||||
holder.onAuthorClick.subscribe(::onAuthorClick);
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
);
|
||||
@@ -211,6 +214,19 @@ class CommentsFragment : MainFragment() {
|
||||
setRepliesOverlayVisible(true, true)
|
||||
}
|
||||
}
|
||||
private fun onAuthorClick(c: IPlatformComment) {
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
return@onAuthorClick;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||
//_fragment.navigate<BrowserFragment>(navUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRepliesClick(c: IPlatformComment) {
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
|
||||
+9
@@ -12,10 +12,12 @@ 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.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
|
||||
@@ -81,6 +83,12 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
StatePlayer.instance.addToQueue(it);
|
||||
}
|
||||
};
|
||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
if (it is IPlatformVideo) {
|
||||
showVideoOptionsOverlay(it)
|
||||
@@ -135,6 +143,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
adapter.onChannelClicked.remove(this);
|
||||
adapter.onAddToClicked.remove(this);
|
||||
adapter.onAddToQueueClicked.remove(this);
|
||||
adapter.onAddToWatchLaterClicked.remove(this);
|
||||
adapter.onLongPress.remove(this);
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -129,7 +129,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
onFilterClick.subscribe(this) {
|
||||
_overlayContainer.let {
|
||||
val filterValuesCopy = HashMap(_filterValues);
|
||||
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy);
|
||||
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy, _channelUrl != null);
|
||||
filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
|
||||
if (changed) {
|
||||
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
|
||||
@@ -170,7 +170,11 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val commonCapabilities = StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
val commonCapabilities =
|
||||
if(_channelUrl == null)
|
||||
StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
else
|
||||
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
val sorts = commonCapabilities?.sorts ?: listOf();
|
||||
if (sorts.size > 1) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
+21
-6
@@ -12,8 +12,10 @@ import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
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.views.AnyInsertedAdapterView
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
@@ -143,6 +145,7 @@ class DownloadsFragment : MainFragment() {
|
||||
|
||||
val activeDownloads = StateDownloads.instance.getDownloading();
|
||||
val playlists = StateDownloads.instance.getCachedPlaylists();
|
||||
val watchLaterDownload = StateDownloads.instance.getWatchLaterDescriptor();
|
||||
val downloaded = StateDownloads.instance.getDownloadedVideos()
|
||||
.filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) };
|
||||
|
||||
@@ -150,23 +153,35 @@ class DownloadsFragment : MainFragment() {
|
||||
_listActiveDownloadsContainer.visibility = GONE;
|
||||
else {
|
||||
_listActiveDownloadsContainer.visibility = VISIBLE;
|
||||
_listActiveDownloadsMeta.text = "(${activeDownloads.size})";
|
||||
_listActiveDownloadsMeta.text = "(${activeDownloads.size} videos)";
|
||||
|
||||
_listActiveDownloads.removeAllViews();
|
||||
for(view in activeDownloads.map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
||||
for(view in activeDownloads.take(4).map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
||||
_listActiveDownloads.addView(view);
|
||||
}
|
||||
|
||||
if(playlists.isEmpty())
|
||||
if(playlists.isEmpty() && watchLaterDownload == null)
|
||||
_listPlaylistsContainer.visibility = GONE;
|
||||
else {
|
||||
_listPlaylistsContainer.visibility = VISIBLE;
|
||||
_listPlaylistsMeta.text = "(${playlists.size} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size }} ${context.getString(R.string.videos).lowercase()})";
|
||||
|
||||
val watchLater = if(watchLaterDownload != null) StatePlaylists.instance.getWatchLater() else listOf();
|
||||
|
||||
_listPlaylistsMeta.text = "(${playlists.size + (if(watchLaterDownload != null) 1 else 0)} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size } + watchLater.size} ${context.getString(R.string.videos).lowercase()})";
|
||||
|
||||
_listPlaylists.removeAllViews();
|
||||
for(view in playlists.map { PlaylistDownloadItem(context, it) }) {
|
||||
if(watchLaterDownload != null) {
|
||||
val pdView = PlaylistDownloadItem(context, "Watch Later", watchLater.firstOrNull()?.thumbnails?.getHQThumbnail(), "WATCHLATER");
|
||||
pdView.setOnClickListener {
|
||||
_frag.navigate<WatchLaterFragment>();
|
||||
}
|
||||
_listPlaylists.addView(pdView);
|
||||
}
|
||||
for(view in playlists.map { PlaylistDownloadItem(context, it.playlist.name, it.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail(), it.playlist) }) {
|
||||
view.setOnClickListener {
|
||||
_frag.navigate<PlaylistFragment>(view.playlist.playlist);
|
||||
if(view.obj is Playlist) {
|
||||
_frag.navigate<PlaylistFragment>(view.obj);
|
||||
}
|
||||
};
|
||||
_listPlaylists.addView(view);
|
||||
}
|
||||
|
||||
+15
-40
@@ -201,14 +201,18 @@ class PlaylistFragment : MainFragment() {
|
||||
showConvertPlaylistButton();
|
||||
}
|
||||
|
||||
updateDownloadState();
|
||||
_playlist?.let {
|
||||
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
|
||||
}
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
updateDownloadState();
|
||||
_playlist?.let {
|
||||
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||
}
|
||||
@@ -217,7 +221,9 @@ class PlaylistFragment : MainFragment() {
|
||||
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
updateDownloadState();
|
||||
_playlist?.let {
|
||||
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||
}
|
||||
@@ -225,6 +231,12 @@ class PlaylistFragment : MainFragment() {
|
||||
};
|
||||
}
|
||||
|
||||
private fun download() {
|
||||
_playlist?.let {
|
||||
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
|
||||
}
|
||||
}
|
||||
|
||||
fun onPause() {
|
||||
StateDownloads.instance.onDownloadsChanged.remove(this);
|
||||
StateDownloads.instance.onDownloadedChanged.remove(this);
|
||||
@@ -268,43 +280,6 @@ class PlaylistFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadState() {
|
||||
val playlist = _playlist ?: return;
|
||||
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == playlist.id };
|
||||
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlist.id);
|
||||
|
||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
|
||||
|
||||
if(isDownloaded && !isDownloading)
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
|
||||
else
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||
|
||||
if(isDownloading) {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if(isDownloaded) {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||
_buttonDownload.setOnClickListener {
|
||||
UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
||||
}
|
||||
}
|
||||
_buttonDownload.setPadding(dp10.toInt());
|
||||
}
|
||||
|
||||
override fun canEdit(): Boolean { return _playlist != null; }
|
||||
|
||||
|
||||
+4
@@ -35,6 +35,7 @@ import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
@@ -211,6 +212,8 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||
|
||||
_textContent.setPlatformPlayerLinkMovementMethod(context);
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
//TODO: add overlay to layout
|
||||
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
@@ -473,6 +476,7 @@ class PostDetailFragment : MainFragment {
|
||||
}
|
||||
|
||||
updateCommentType(true);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
fun setPostOverview(value: IPlatformPost) {
|
||||
|
||||
+28
-8
@@ -12,6 +12,7 @@ import android.webkit.CookieManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
@@ -107,17 +108,20 @@ class SourceDetailFragment : MainFragment() {
|
||||
fun onHide() {
|
||||
val id = _config?.id ?: return;
|
||||
|
||||
if(_settingsChanged && _settings != null) {
|
||||
_settingsChanged = false;
|
||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||
reloadSource(id);
|
||||
|
||||
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||
}
|
||||
var shouldReload = false;
|
||||
if(_settingsAppChanged) {
|
||||
_settingsAppForm.setObjectValues();
|
||||
StatePlugins.instance.savePlugin(id);
|
||||
shouldReload = true;
|
||||
}
|
||||
if(_settingsChanged && _settings != null) {
|
||||
_settingsChanged = false;
|
||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||
shouldReload = true;
|
||||
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||
}
|
||||
if(shouldReload)
|
||||
reloadSource(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -137,9 +141,25 @@ class SourceDetailFragment : MainFragment() {
|
||||
//App settings
|
||||
try {
|
||||
_settingsAppForm.fromObject(source.descriptor.appSettings);
|
||||
if(source.config.developerSubmitUrl.isNullOrEmpty()) {
|
||||
val field = _settingsAppForm.findField("devSubmit");
|
||||
field?.setValue(false);
|
||||
if(field is View)
|
||||
field.isVisible = false;
|
||||
}
|
||||
_settingsAppForm.onChanged.clear();
|
||||
_settingsAppForm.onChanged.subscribe { _, _ ->
|
||||
_settingsAppForm.onChanged.subscribe { field, value ->
|
||||
_settingsAppChanged = true;
|
||||
if(field.descriptor?.id == "devSubmit") {
|
||||
if(value is Boolean && value) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow,
|
||||
"Are you sure you trust the developer?",
|
||||
"Developers may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.\nThe following domain is used:",
|
||||
source.config.developerSubmitUrl ?: "", 0,
|
||||
UIDialogs.Action("Cancel", { field.setValue(false); }, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Enable", { }, UIDialogs.ActionStyle.DANGEROUS));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to load app settings form from plugin settings", e)
|
||||
|
||||
+20
-1
@@ -23,6 +23,7 @@ import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.WindowManager
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
@@ -124,6 +125,7 @@ import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||
import com.futo.platformplayer.views.overlays.WebviewOverlay
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
@@ -244,6 +246,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _container_content_replies: RepliesOverlay;
|
||||
private val _container_content_description: DescriptionOverlay;
|
||||
private val _container_content_liveChat: LiveChatOverlay;
|
||||
private val _container_content_browser: WebviewOverlay;
|
||||
private val _container_content_support: SupportOverlay;
|
||||
|
||||
private var _container_content_current: View;
|
||||
@@ -349,7 +352,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
||||
_container_content_description = findViewById(R.id.videodetail_container_description);
|
||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||
_container_content_support = findViewById(R.id.videodetail_container_support)
|
||||
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
||||
|
||||
_textComments = findViewById(R.id.text_comments);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
@@ -624,6 +628,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
|
||||
_description_viewMore.setOnClickListener {
|
||||
switchContentView(_container_content_description);
|
||||
@@ -644,6 +649,20 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_container_content_current = _container_content_main;
|
||||
|
||||
_commentsList.onAuthorClick.subscribe { c ->
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
return@subscribe;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||
fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||
//_container_content_browser.goto(navUrl);
|
||||
//switchContentView(_container_content_browser);
|
||||
}
|
||||
};
|
||||
_commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
var metadata = "";
|
||||
|
||||
+46
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
@@ -8,10 +9,17 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.setPadding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.assume
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||
|
||||
abstract class VideoListEditorView : LinearLayout {
|
||||
@@ -85,6 +93,44 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
|
||||
}
|
||||
|
||||
protected fun updateDownloadState(groupType: String, playlistId: String, onDownload: ()->Unit) {
|
||||
//val playlist = _playlist ?: return;
|
||||
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == groupType && it.groupID == playlistId };
|
||||
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlistId);
|
||||
|
||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
|
||||
|
||||
if(isDownloaded && !isDownloading)
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
|
||||
else
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||
|
||||
if(isDownloading) {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if(isDownloaded) {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||
_buttonDownload.setOnClickListener {
|
||||
onDownload();
|
||||
//UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
||||
}
|
||||
}
|
||||
_buttonDownload.setPadding(dp10.toInt());
|
||||
}
|
||||
|
||||
protected fun setName(name: String?) {
|
||||
_textName.text = name ?: "";
|
||||
|
||||
+41
@@ -5,10 +5,17 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WatchLaterFragment : MainFragment() {
|
||||
override val isMainView : Boolean = true;
|
||||
@@ -28,6 +35,11 @@ class WatchLaterFragment : MainFragment() {
|
||||
return view;
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
_view?.onResume();
|
||||
}
|
||||
|
||||
override fun onDestroyMainView() {
|
||||
super.onDestroyMainView();
|
||||
_view = null;
|
||||
@@ -45,6 +57,34 @@ class WatchLaterFragment : MainFragment() {
|
||||
fun onShown() {
|
||||
setName("Watch Later");
|
||||
setVideos(StatePlaylists.instance.getWatchLater(), true);
|
||||
|
||||
setButtonDownloadVisible(true);
|
||||
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||
}
|
||||
|
||||
fun onResume(){
|
||||
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||
}
|
||||
}
|
||||
};
|
||||
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fun download(){
|
||||
UISlideOverlays.showDownloadWatchlaterOverlay(overlayContainer);
|
||||
}
|
||||
|
||||
override fun onPlayAllClick() {
|
||||
@@ -76,6 +116,7 @@ class WatchLaterFragment : MainFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "WatchLaterFragment";
|
||||
fun newInstance() = WatchLaterFragment().apply {}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,9 @@ class Subscription {
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastPeekVideo : OffsetDateTime = OffsetDateTime.MIN;
|
||||
|
||||
//Last video interval
|
||||
var uploadInterval : Int = 0;
|
||||
var uploadStreamInterval : Int = 0;
|
||||
@@ -126,6 +129,7 @@ class Subscription {
|
||||
else if(lastVideo.year > 3000)
|
||||
lastVideo = OffsetDateTime.MIN;
|
||||
lastVideoUpdate = OffsetDateTime.now();
|
||||
lastPeekVideo = OffsetDateTime.MIN;
|
||||
}
|
||||
ResultCapabilities.TYPE_MIXED -> {
|
||||
uploadInterval = interval;
|
||||
@@ -134,6 +138,7 @@ class Subscription {
|
||||
else if(lastVideo.year > 3000)
|
||||
lastVideo = OffsetDateTime.MIN;
|
||||
lastVideoUpdate = OffsetDateTime.now();
|
||||
lastPeekVideo = OffsetDateTime.MIN;
|
||||
}
|
||||
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
|
||||
uploadInterval = interval;
|
||||
|
||||
@@ -270,7 +270,7 @@ class DownloadService : Service() {
|
||||
|
||||
fun closeDownloadSession() {
|
||||
Logger.i(TAG, "closeDownloadSession");
|
||||
stopForeground(STOP_FOREGROUND_DETACH);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID);
|
||||
stopService();
|
||||
_started = false;
|
||||
|
||||
@@ -188,7 +188,7 @@ class ExportingService : Service() {
|
||||
|
||||
fun closeExportSession() {
|
||||
Logger.i(TAG, "closeExportSession");
|
||||
stopForeground(STOP_FOREGROUND_DETACH);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
_notificationManager?.cancel(EXPORT_NOTIF_ID);
|
||||
stopService();
|
||||
_started = false;
|
||||
|
||||
@@ -54,6 +54,9 @@ import kotlin.system.measureTimeMillis
|
||||
class StateApp {
|
||||
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
||||
|
||||
val sessionId = UUID.randomUUID().toString();
|
||||
|
||||
|
||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||
if(isValidStorageUri(context, generalUri))
|
||||
|
||||
@@ -97,6 +97,9 @@ class StateDownloads {
|
||||
}
|
||||
}
|
||||
|
||||
fun getWatchLaterDescriptor(): PlaylistDownloadDescriptor? {
|
||||
return _downloadPlaylists.getItems().find { it.id == VideoDownload.GROUP_WATCHLATER };
|
||||
}
|
||||
fun getCachedPlaylists(): List<PlaylistDownloaded> {
|
||||
return _downloadPlaylists.getItems()
|
||||
.map { Pair(it, StatePlaylists.instance.getPlaylist(it.id)) }
|
||||
@@ -124,19 +127,32 @@ class StateDownloads {
|
||||
val pdl = getPlaylistDownload(id);
|
||||
if(pdl != null)
|
||||
_downloadPlaylists.delete(pdl);
|
||||
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||
.forEach { removeDownload(it) };
|
||||
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||
.forEach { deleteCachedVideo(it.id) };
|
||||
if(id == VideoDownload.GROUP_WATCHLATER) {
|
||||
getDownloading().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
|
||||
.forEach { removeDownload(it) };
|
||||
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
|
||||
.forEach { deleteCachedVideo(it.id) };
|
||||
}
|
||||
else {
|
||||
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||
.forEach { removeDownload(it) };
|
||||
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||
.forEach { deleteCachedVideo(it.id) };
|
||||
}
|
||||
}
|
||||
|
||||
fun getDownloadedVideos(): List<VideoLocal> {
|
||||
return _downloaded.getItems();
|
||||
}
|
||||
fun getDownloadedVideosPlaylist(str: String): List<VideoLocal> {
|
||||
val videos = _downloaded.findItems { it.groupID == str };
|
||||
return videos;
|
||||
}
|
||||
|
||||
fun getDownloadPlaylists(): List<PlaylistDownloadDescriptor> {
|
||||
return _downloadPlaylists.getItems();
|
||||
}
|
||||
|
||||
fun isPlaylistCached(id: String): Boolean {
|
||||
return getDownloadPlaylists().any{it.id == id};
|
||||
}
|
||||
@@ -177,6 +193,21 @@ class StateDownloads {
|
||||
DownloadService.getOrCreateService(it);
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForOutdatedPlaylistVideos(playlistId: String) {
|
||||
val playlistVideos = if(playlistId == VideoDownload.GROUP_WATCHLATER)
|
||||
(if(getWatchLaterDescriptor() != null) StatePlaylists.instance.getWatchLater() else listOf())
|
||||
else
|
||||
getCachedPlaylist(playlistId)?.playlist?.videos ?: return;
|
||||
val playlistVideosDownloaded = getDownloadedVideosPlaylist(playlistId);
|
||||
val urls = playlistVideos.map { it.url }.toHashSet();
|
||||
for(item in playlistVideosDownloaded) {
|
||||
if(!urls.contains(item.url)) {
|
||||
Logger.i(TAG, "Playlist [${playlistId}] deleting removed video [${item.name}]");
|
||||
deleteCachedVideo(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun checkForOutdatedPlaylists(): Boolean {
|
||||
var hasChanged = false;
|
||||
val playlistsDownloaded = getCachedPlaylists();
|
||||
@@ -192,9 +223,59 @@ class StateDownloads {
|
||||
else
|
||||
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
|
||||
}
|
||||
val downloadWatchLater = getWatchLaterDescriptor();
|
||||
if(downloadWatchLater != null) {
|
||||
continueDownloadWatchLater(downloadWatchLater);
|
||||
}
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
fun continueDownloadWatchLater(playlistDownload: PlaylistDownloadDescriptor) {
|
||||
var hasNew = false;
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
for(item in watchLater) {
|
||||
val existing = getCachedVideo(item.id);
|
||||
|
||||
if(!playlistDownload.shouldDownload(item)) {
|
||||
Logger.i(TAG, "Not downloading for watchlater [${playlistDownload.id}] Video [${item.name}]:${item.url}")
|
||||
continue;
|
||||
}
|
||||
if(existing == null) {
|
||||
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != null };
|
||||
if(ongoingDownload != null) {
|
||||
Logger.i(TAG, "New watchlater video (already downloading) ${item.name}");
|
||||
ongoingDownload.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||
ongoingDownload.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "New watchlater video ${item.name}");
|
||||
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
|
||||
.withGroup(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER), false);
|
||||
hasNew = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "New watchlater video (already downloaded) ${item.name}");
|
||||
if(existing.groupID == null) {
|
||||
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.add(existing.id);
|
||||
}
|
||||
_downloaded.save(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(watchLater.isNotEmpty() && Settings.instance.downloads.shouldDownload()) {
|
||||
if(hasNew) {
|
||||
UIDialogs.toast("Downloading [Watch Later]")
|
||||
StateApp.withContext {
|
||||
DownloadService.getOrCreateService(it);
|
||||
}
|
||||
}
|
||||
onDownloadsChanged.emit();
|
||||
}
|
||||
}
|
||||
fun continueDownload(playlistDownload: PlaylistDownloadDescriptor, playlist: Playlist) {
|
||||
var hasNew = false;
|
||||
for(item in playlist.videos) {
|
||||
@@ -240,6 +321,11 @@ class StateDownloads {
|
||||
onDownloadsChanged.emit();
|
||||
}
|
||||
}
|
||||
fun downloadWatchLater(targetPixelCount: Long?, targetBitrate: Long?) {
|
||||
val playlistDownload = PlaylistDownloadDescriptor(VideoDownload.GROUP_WATCHLATER, targetPixelCount, targetBitrate);
|
||||
_downloadPlaylists.save(playlistDownload);
|
||||
continueDownloadWatchLater(playlistDownload);
|
||||
}
|
||||
fun download(playlist: Playlist, targetPixelcount: Long?, targetBitrate: Long?) {
|
||||
val playlistDownload = PlaylistDownloadDescriptor(playlist.id, targetPixelcount, targetBitrate);
|
||||
_downloadPlaylists.save(playlistDownload);
|
||||
|
||||
@@ -529,12 +529,23 @@ class StatePlatform {
|
||||
}
|
||||
|
||||
fun getCommonSearchCapabilities(clientIds: List<String>): ResultCapabilities? {
|
||||
return getCommonSearchCapabilitiesType(clientIds){
|
||||
it.getSearchCapabilities()
|
||||
};
|
||||
}
|
||||
fun getCommonSearchChannelContentsCapabilities(clientIds: List<String>): ResultCapabilities? {
|
||||
return getCommonSearchCapabilitiesType(clientIds){
|
||||
it.getSearchChannelContentsCapabilities()
|
||||
};
|
||||
}
|
||||
|
||||
fun getCommonSearchCapabilitiesType(clientIds: List<String>, capabilitiesGetter: (client: IPlatformClient)-> ResultCapabilities): ResultCapabilities? {
|
||||
try {
|
||||
Logger.i(TAG, "Platform - getCommonSearchCapabilities");
|
||||
|
||||
val clients = getEnabledClients().filter { clientIds.contains(it.id) };
|
||||
val c = clients.firstOrNull() ?: return null;
|
||||
val cap = c.getSearchCapabilities();
|
||||
val cap = capabilitiesGetter(c)//c.getSearchCapabilities();
|
||||
|
||||
//var types = arrayListOf<String>();
|
||||
var sorts = cap.sorts.toMutableList();
|
||||
@@ -544,7 +555,7 @@ class StatePlatform {
|
||||
val filtersToRemove = arrayListOf<Int>();
|
||||
|
||||
for (i in 1 until clients.size) {
|
||||
val clientSearchCapabilities = clients[i].getSearchCapabilities();
|
||||
val clientSearchCapabilities = capabilitiesGetter(clients[i]);//.getSearchCapabilities();
|
||||
|
||||
for (j in 0 until sorts.size) {
|
||||
if (!clientSearchCapabilities.sorts.contains(sorts[j])) {
|
||||
@@ -665,8 +676,11 @@ class StatePlatform {
|
||||
|
||||
val pagerResult: IPager<IPlatformContent>;
|
||||
if(!clientCapabilities.hasType(ResultCapabilities.TYPE_MIXED) &&
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS) &&
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_STREAMS)) {
|
||||
( clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS) ||
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_STREAMS) ||
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE) ||
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_POSTS)
|
||||
)) {
|
||||
val toQuery = mutableListOf<String>();
|
||||
if(clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||
toQuery.add(ResultCapabilities.TYPE_VIDEOS);
|
||||
@@ -786,6 +800,10 @@ class StatePlatform {
|
||||
return client.getChannelContents(channelUrl, type, ordering) ;
|
||||
}
|
||||
|
||||
fun peekChannelContents(baseClient: IPlatformClient, channelUrl: String, type: String?): List<IPlatformContent> {
|
||||
val client = _channelClientPool.getClientPooled(baseClient, Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||
return client.peekChannelContents(channelUrl, type) ;
|
||||
}
|
||||
|
||||
fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel {
|
||||
val channel = getChannelClient(url).getChannel(url);
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.ReconstructionException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -66,6 +67,10 @@ class StatePlaylists {
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
|
||||
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||
}
|
||||
}
|
||||
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
||||
synchronized(_watchlistStore) {
|
||||
@@ -74,6 +79,10 @@ class StatePlaylists {
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
|
||||
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||
}
|
||||
}
|
||||
fun addToWatchLater(video: SerializedPlatformVideo) {
|
||||
synchronized(_watchlistStore) {
|
||||
@@ -82,6 +91,8 @@ class StatePlaylists {
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}
|
||||
|
||||
fun getLastPlayedPlaylist() : Playlist? {
|
||||
@@ -131,6 +142,11 @@ class StatePlaylists {
|
||||
fun createOrUpdatePlaylist(playlist: Playlist) {
|
||||
playlist.dateUpdate = OffsetDateTime.now();
|
||||
playlistStore.saveAsync(playlist, true);
|
||||
if(playlist.id.isNotEmpty()) {
|
||||
if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun addToPlaylist(id: String, video: IPlatformVideo) {
|
||||
synchronized(playlistStore) {
|
||||
@@ -143,6 +159,9 @@ class StatePlaylists {
|
||||
|
||||
fun removePlaylist(playlist: Playlist) {
|
||||
playlistStore.delete(playlist);
|
||||
if(StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||
}
|
||||
}
|
||||
|
||||
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {
|
||||
|
||||
+21
-4
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer.subscription
|
||||
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.getNowDiffHours
|
||||
@@ -7,6 +8,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
|
||||
class SmartSubscriptionAlgorithm(
|
||||
@@ -70,18 +72,30 @@ class SmartSubscriptionAlgorithm(
|
||||
} else {
|
||||
val fetchTasks = mutableListOf<SubscriptionTask>();
|
||||
val cacheTasks = mutableListOf<SubscriptionTask>();
|
||||
var peekTasks = mutableListOf<SubscriptionTask>();
|
||||
|
||||
for(task in clientTasks.second) {
|
||||
if (!task.fromCache && fetchTasks.size < limit) {
|
||||
fetchTasks.add(task);
|
||||
} else {
|
||||
task.fromCache = true;
|
||||
cacheTasks.add(task);
|
||||
if(peekTasks.size < 100 &&
|
||||
Settings.instance.subscriptions.peekChannelContents &&
|
||||
(task.sub.lastPeekVideo.year < 1971 || task.sub.lastPeekVideo < task.sub.lastVideoUpdate) &&
|
||||
task.client.capabilities.hasPeekChannelContents &&
|
||||
task.client.getPeekChannelTypes().contains(task.type)) {
|
||||
task.fromPeek = true;
|
||||
task.fromCache = true;
|
||||
peekTasks.add(task);
|
||||
}
|
||||
else {
|
||||
task.fromCache = true;
|
||||
cacheTasks.add(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.i(TAG, "Subscription Client Budget [${clientTasks.first.name}]: ${fetchTasks.size}/${limit}")
|
||||
|
||||
finalTasks.addAll(fetchTasks + cacheTasks);
|
||||
finalTasks.addAll(fetchTasks + peekTasks + cacheTasks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +129,9 @@ class SmartSubscriptionAlgorithm(
|
||||
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
|
||||
val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble();
|
||||
|
||||
return (expectedHours * 100).toInt();
|
||||
if((type == ResultCapabilities.TYPE_MIXED || type == ResultCapabilities.TYPE_VIDEOS) && (sub.lastPeekVideo.year > 1970 && sub.lastPeekVideo > sub.lastVideoUpdate))
|
||||
return 0;
|
||||
else
|
||||
return (expectedHours * 100).toInt();
|
||||
}
|
||||
}
|
||||
+31
-5
@@ -24,6 +24,7 @@ import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
@@ -48,15 +49,17 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val tasksGrouped = tasks.groupBy { it.client }
|
||||
|
||||
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
|
||||
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } })" }.joinToString("\n"));
|
||||
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } - it.value.count { it.fromPeek && it.fromCache }}), Peek(${it.value.count { it.fromPeek }})" }.joinToString("\n"));
|
||||
|
||||
try {
|
||||
for(clientTasks in tasksGrouped) {
|
||||
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||
val clientTaskCount = clientTasks.value.count { !it.fromCache };
|
||||
val clientCacheCount = clientTasks.value.count { it.fromCache && !it.fromPeek };
|
||||
val clientPeekCount = clientTasks.value.count { it.fromPeek };
|
||||
val limit = clientTasks.key.getSubscriptionRateLimit();
|
||||
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
||||
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
||||
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). " +
|
||||
"(${if(clientPeekCount > 0) "${clientPeekCount} peek, " else ""}${clientCacheCount} cached)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,8 +138,30 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
|
||||
for(task in tasks) {
|
||||
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
||||
if(task.fromPeek) {
|
||||
try {
|
||||
|
||||
val time = measureTimeMillis {
|
||||
val peekResults = StatePlatform.instance.peekChannelContents(task.client, task.url, task.type);
|
||||
val mostRecent = peekResults.firstOrNull();
|
||||
task.sub.lastPeekVideo = mostRecent?.datetime ?: OffsetDateTime.MIN;
|
||||
task.sub.saveAsync();
|
||||
val cacheItems = peekResults.filter { it.datetime != null && it.datetime!! > task.sub.lastVideoUpdate };
|
||||
//Fix for current situation
|
||||
for(item in cacheItems) {
|
||||
if(item.author.thumbnail.isNullOrEmpty())
|
||||
item.author.thumbnail = task.sub.channel.thumbnail;
|
||||
}
|
||||
StateCache.instance.cacheContents(cacheItems, false);
|
||||
}
|
||||
Logger.i("StateSubscriptions", "Subscription peek [${task.sub.channel.name}]:${task.type} results in ${time}ms");
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||
}
|
||||
}
|
||||
synchronized(cachedChannels) {
|
||||
if(task.fromCache) {
|
||||
if(task.fromCache || task.fromPeek) {
|
||||
finished++;
|
||||
onProgress.emit(finished, forkTasks.size);
|
||||
if(cachedChannels.contains(task.url)) {
|
||||
@@ -218,6 +243,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val url: String,
|
||||
val type: String,
|
||||
var fromCache: Boolean = false,
|
||||
var fromPeek: Boolean = false,
|
||||
var urgency: Int = 0
|
||||
);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformContent>();
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||
val onLongPress = Event1<IPlatformContent>();
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
@@ -56,6 +57,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit);
|
||||
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit);
|
||||
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit);
|
||||
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit);
|
||||
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit);
|
||||
};
|
||||
1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) };
|
||||
|
||||
@@ -48,6 +48,7 @@ class CommentViewHolder : ViewHolder {
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var onAuthorClick = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
@@ -95,6 +96,19 @@ class CommentViewHolder : ViewHolder {
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
|
||||
_creatorThumbnail.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
|
||||
_creatorThumbnail.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
_textAuthor.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
_buttonReplies.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onRepliesClick.emit(c);
|
||||
|
||||
+12
-3
@@ -53,9 +53,10 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
hideLikesDislikesReplies()
|
||||
}
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var onClick = Event1<IPlatformComment>();
|
||||
val onRepliesClick = Event1<IPlatformComment>();
|
||||
val onDelete = Event1<IPlatformComment>();
|
||||
val onClick = Event1<IPlatformComment>();
|
||||
val onAuthorClick = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
@@ -99,6 +100,14 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
|
||||
_creatorThumbnail.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
_textAuthor.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
_buttonReplies.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onRepliesClick.emit(c);
|
||||
|
||||
+4
@@ -39,6 +39,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformContent>();
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||
val onLongPress = Event1<IPlatformContent>();
|
||||
|
||||
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
|
||||
@@ -95,6 +96,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
||||
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
||||
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
|
||||
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
|
||||
};
|
||||
private fun createLockedViewHolder(viewGroup: ViewGroup): PreviewLockedViewHolder = PreviewLockedViewHolder(viewGroup, _feedStyle).apply {
|
||||
this.onLockedUrlClicked.subscribe(this@PreviewContentListAdapter.onUrlClicked::emit);
|
||||
@@ -106,6 +108,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
||||
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
||||
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
|
||||
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
|
||||
this.onLongPress.subscribe(this@PreviewContentListAdapter.onLongPress::emit);
|
||||
};
|
||||
private fun createPlaylistViewHolder(viewGroup: ViewGroup): PreviewPlaylistViewHolder = PreviewPlaylistViewHolder(viewGroup, _feedStyle).apply {
|
||||
@@ -161,6 +164,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
onChannelClicked.clear();
|
||||
onAddToClicked.clear();
|
||||
onAddToQueueClicked.clear();
|
||||
onAddToWatchLaterClicked.clear();
|
||||
}
|
||||
|
||||
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformVideo>();
|
||||
val onAddToQueueClicked = Event1<IPlatformVideo>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
|
||||
|
||||
override val content: IPlatformContent? get() = view.content;
|
||||
private val view: PreviewNestedVideoView get() = itemView as PreviewNestedVideoView;
|
||||
@@ -31,6 +32,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
|
||||
view.onChannelClicked.subscribe(onChannelClicked::emit);
|
||||
view.onAddToClicked.subscribe(onAddToClicked::emit);
|
||||
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
|
||||
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+4
-1
@@ -61,6 +61,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
protected val _layoutDownloaded: FrameLayout;
|
||||
|
||||
protected val _button_add_to_queue : View;
|
||||
protected val _button_add_to_watch_later : View;
|
||||
protected val _button_add_to : View;
|
||||
|
||||
protected val _exoPlayer: PlayerManager?;
|
||||
@@ -80,6 +81,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformVideo>();
|
||||
val onAddToQueueClicked = Event1<IPlatformVideo>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
|
||||
|
||||
var currentVideo: IPlatformVideo? = null
|
||||
private set
|
||||
@@ -104,6 +106,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
_containerDuration = findViewById(R.id.thumbnail_duration_container);
|
||||
_containerLive = findViewById(R.id.thumbnail_live_container);
|
||||
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
|
||||
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
|
||||
_button_add_to = findViewById(R.id.button_add_to);
|
||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
||||
@@ -124,7 +127,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
_textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
|
||||
_button_add_to.setOnClickListener { currentVideo?.let { onAddToClicked.emit(it) } };
|
||||
_button_add_to_queue.setOnClickListener { currentVideo?.let { onAddToQueueClicked.emit(it) } };
|
||||
|
||||
_button_add_to_watch_later.setOnClickListener { currentVideo?.let { onAddToWatchLaterClicked.emit(it); } }
|
||||
}
|
||||
|
||||
protected open fun inflate(feedStyle: FeedStyle) {
|
||||
|
||||
+2
@@ -18,6 +18,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformVideo>();
|
||||
val onAddToQueueClicked = Event1<IPlatformVideo>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
|
||||
val onLongPress = Event1<IPlatformVideo>();
|
||||
|
||||
//val context: Context;
|
||||
@@ -34,6 +35,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
|
||||
view.onChannelClicked.subscribe(onChannelClicked::emit);
|
||||
view.onAddToClicked.subscribe(onAddToClicked::emit);
|
||||
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
|
||||
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
|
||||
view.onLongPress.subscribe(onLongPress::emit);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,16 +9,16 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.models.PlaylistDownloaded
|
||||
|
||||
class PlaylistDownloadItem(context: Context, val playlist: PlaylistDownloaded): LinearLayout(context) {
|
||||
class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumbnail: String?, val obj: Any): LinearLayout(context) {
|
||||
init { inflate(context, R.layout.list_downloaded_playlist, this) }
|
||||
|
||||
var imageView: ImageView = findViewById(R.id.downloaded_playlist_image);
|
||||
var imageText: TextView = findViewById(R.id.downloaded_playlist_name);
|
||||
|
||||
init {
|
||||
imageText.text = playlist.playlist.name;
|
||||
imageText.text = playlistName;
|
||||
Glide.with(imageView)
|
||||
.load(playlist.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail())
|
||||
.load(playlistThumbnail)
|
||||
.crossfade()
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.webkit.WebView
|
||||
import android.widget.LinearLayout
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.SupportView
|
||||
|
||||
class WebviewOverlay : LinearLayout {
|
||||
val onClose = Event0();
|
||||
|
||||
private val _topbar: OverlayTopbar;
|
||||
private val _webview: WebView;
|
||||
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.overlay_webview, this)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_webview = findViewById(R.id.webview);
|
||||
_webview.settings.javaScriptEnabled = true;
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
}
|
||||
|
||||
fun goto(url: String) {
|
||||
Logger.i("WebviewOverlay", "Loading [${url}]");
|
||||
_topbar.setInfo(url, "");
|
||||
_webview.loadUrl(url);
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
_topbar.onClose.remove(this);
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -28,13 +28,17 @@ class SlideUpMenuFilters {
|
||||
private var _changed: Boolean = false;
|
||||
private val _lifecycleScope: CoroutineScope;
|
||||
|
||||
private var _isChannelSearch = false;
|
||||
|
||||
var commonCapabilities: ResultCapabilities? = null;
|
||||
|
||||
constructor(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>) {
|
||||
|
||||
constructor(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false) {
|
||||
_lifecycleScope = lifecycleScope;
|
||||
_container = container;
|
||||
_enabledClientsIds = enabledClientsIds;
|
||||
_filterValues = filterValues;
|
||||
_isChannelSearch = isChannelSearch;
|
||||
_slideUpMenuOverlay = SlideUpMenuOverlay(_container.context, _container, container.context.getString(R.string.filters), container.context.getString(R.string.done), true, listOf());
|
||||
_slideUpMenuOverlay.onOK.subscribe {
|
||||
onOK.emit(_enabledClientsIds, _changed);
|
||||
@@ -47,7 +51,10 @@ class SlideUpMenuFilters {
|
||||
private fun updateCommonCapabilities() {
|
||||
_lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val caps = StatePlatform.instance.getCommonSearchCapabilities(_enabledClientsIds);
|
||||
val caps = if(!_isChannelSearch)
|
||||
StatePlatform.instance.getCommonSearchCapabilities(_enabledClientsIds);
|
||||
else
|
||||
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(_enabledClientsIds);
|
||||
synchronized(_filterValues) {
|
||||
if (caps != null) {
|
||||
val keysToRemove = arrayListOf<String>();
|
||||
|
||||
@@ -88,6 +88,7 @@ class CommentsList : ConstraintLayout {
|
||||
private val _layoutScrollToTop: FrameLayout;
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onAuthorClick = Event1<IPlatformComment>();
|
||||
var onCommentsLoaded = Event1<Int>();
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
@@ -120,6 +121,7 @@ class CommentsList : ConstraintLayout {
|
||||
childViewHolderFactory = { viewGroup, _ ->
|
||||
val holder = CommentViewHolder(viewGroup);
|
||||
holder.onRepliesClick.subscribe { c -> onRepliesClick.emit(c) };
|
||||
holder.onAuthorClick.subscribe { c -> onAuthorClick.emit(c) };
|
||||
holder.onDelete.subscribe(::onDelete);
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M612,668L668,612L520,464L520,280L440,280L440,496L612,668ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q613,800 706.5,706.5Q800,613 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,613 253.5,706.5Q347,800 480,800Z"/>
|
||||
</vector>
|
||||
@@ -13,7 +13,7 @@
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_construction" />
|
||||
app:srcCompat="@drawable/foreground" />
|
||||
|
||||
<!--<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
@@ -22,17 +22,19 @@
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_futo_logo_text" />-->
|
||||
|
||||
<!--
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="28dp"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="TEST BUILD"
|
||||
android:textSize="22dp"
|
||||
android:layout_marginTop="-2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="Grayjay"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"/>-->
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
<!--
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
@@ -54,7 +56,7 @@
|
||||
android:text="@string/construction"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="-8dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>-->
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -542,6 +542,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.WebviewOverlay
|
||||
android:id="@+id/videodetail_container_webview"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||
android:id="@+id/videodetail_container_queue"
|
||||
android:visibility="gone"
|
||||
|
||||
@@ -226,10 +226,18 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_watch_later"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
app:srcCompat="@drawable/ic_clock_white" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
@@ -242,20 +250,18 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_to"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:contentDescription="@string/options"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp" />
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="16dp"
|
||||
android:paddingTop="1dp"
|
||||
android:src="@drawable/ic_settings" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -262,45 +262,53 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="6dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_watch_later"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
app:srcCompat="@drawable/ic_clock_white" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:paddingTop="7dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:paddingBottom="3dp"
|
||||
app:srcCompat="@drawable/ic_queue_16dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue" />
|
||||
app:srcCompat="@drawable/ic_queue_16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_to"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/options" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="16dp"
|
||||
android:paddingTop="1dp"
|
||||
android:src="@drawable/ic_settings" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/options"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@color/transparent"
|
||||
android:textSize="12dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:layout_marginEnd="4dp"/>
|
||||
android:text="@string/options"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -150,14 +150,11 @@
|
||||
android:padding="5dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/options" />
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="16dp"
|
||||
android:paddingTop="1dp"
|
||||
android:src="@drawable/ic_settings" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -180,13 +177,28 @@
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="7dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_watch_later"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:src="@drawable/ic_clock_white"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingLeft="9dp"
|
||||
android:paddingRight="9dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_name"
|
||||
android:layout_width="fill_parent"
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -215,13 +216,28 @@
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="7dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_watch_later"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:src="@drawable/ic_clock_white"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingLeft="9dp"
|
||||
android:paddingRight="9dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_name"
|
||||
android:layout_width="fill_parent"
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="300dp"
|
||||
tools:text="Queue" />
|
||||
|
||||
<TextView
|
||||
@@ -24,16 +27,21 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/text_name"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_container"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="#ACACAC"
|
||||
android:textSize="13dp"
|
||||
android:layout_marginLeft="15dp"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:layout_marginRight="45dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="3 videos" />
|
||||
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||
android:id="@+id/topbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
app:title="Web"
|
||||
app:metadata=""
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
</WebView>
|
||||
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -342,6 +342,8 @@
|
||||
<string name="fetch_on_tab_opened_description">Fetch new results when the tab is opened (if no results yet, disabling is not recommended unless you have issues)</string>
|
||||
<string name="always_reload_from_cache">Always reload from cache</string>
|
||||
<string name="always_reload_from_cache_description">This is not recommended, but a possible workaround for some issues.</string>
|
||||
<string name="peek_channel_contents">Peek Channel Contents</string>
|
||||
<string name="peek_channel_contents_description">Peek channel contents if supported by plugin of rate-limited calls, may increase subscription reload time.</string>
|
||||
<string name="get_answers_to_common_questions">Get answers to common questions</string>
|
||||
<string name="give_feedback_on_the_application">Give feedback on the application</string>
|
||||
<string name="info">Info</string>
|
||||
@@ -487,6 +489,9 @@
|
||||
<string name="visibility">Visibility</string>
|
||||
<string name="check_for_updates_setting">Check for updates</string>
|
||||
<string name="check_for_updates_setting_description">If a plugin should be checked for updates on startup</string>
|
||||
<string name="allow_developer_submit">Allow Developer Submissions</string>
|
||||
<string name="allow_developer_submit_description">Allows the developer to send data to their server, be careful as this might include sensitive data.</string>
|
||||
<string name="allow_developer_submit_warning">Make sure you trust the developer. They may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.</string>
|
||||
<string name="ratelimit">Rate-limit</string>
|
||||
<string name="ratelimit_description">Settings related to rate-limiting this plugin\'s behavior</string>
|
||||
<string name="ratelimit_sub_setting">Rate-limit Subscriptions</string>
|
||||
|
||||
Submodule app/src/stable/assets/sources/patreon updated: bc13b38411...cee1fda4e8
Submodule app/src/stable/assets/sources/youtube updated: bef199baa9...cac2740844
Submodule app/src/unstable/assets/sources/patreon updated: bc13b38411...cee1fda4e8
Submodule app/src/unstable/assets/sources/youtube updated: bef199baa9...cac2740844
Reference in New Issue
Block a user