Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Koen 2023-12-18 15:48:17 +01:00
commit d63627bd61
81 changed files with 2474 additions and 169 deletions

View file

@ -1,15 +1,28 @@
package com.futo.platformplayer
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
@UnstableApi
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier();
return if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else {
DefaultHttpDataSource.Factory();
}
}
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());

View file

@ -0,0 +1,20 @@
package com.futo.platformplayer
class PresetImages {
companion object {
val images = mapOf<String, Int>(
Pair("xp_book", R.drawable.xp_book),
Pair("xp_forest", R.drawable.xp_forest),
Pair("xp_code", R.drawable.xp_code),
Pair("xp_controller", R.drawable.xp_controller),
Pair("xp_laptop", R.drawable.xp_laptop)
);
fun getPresetResIdByName(name: String): Int {
return images[name] ?: -1;
}
fun getPresetNameByResId(id: Int): String? {
return images.entries.find { it.value == id }?.key;
}
}
}

View file

@ -261,20 +261,23 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL;
}
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
var showSubscriptionGroups: Boolean = true;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true;
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 7)
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true;
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 8)
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true;
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 9)
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10)
@DropdownFieldOptionsId(R.array.background_interval)
var subscriptionsBackgroundUpdateInterval: Int = 0;
@ -290,7 +293,7 @@ class Settings : FragmentedStorageFileJson() {
};
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 10)
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11)
@DropdownFieldOptionsId(R.array.thread_count)
var subscriptionConcurrency: Int = 3;
@ -298,17 +301,17 @@ class Settings : FragmentedStorageFileJson() {
return threadIndexToCount(subscriptionConcurrency);
}
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 11)
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false;
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 12)
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true;
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 13)
@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, 14)
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear();

View file

@ -3,8 +3,10 @@ package com.futo.platformplayer
import android.content.ContentResolver
import android.view.View
import android.view.ViewGroup
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
@ -17,10 +19,13 @@ 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.downloads.VideoLocal
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
@ -37,6 +42,7 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.futo.platformplayer.views.pills.RoundButton
import com.futo.platformplayer.views.pills.RoundButtonGroup
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
import isDownloadable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -46,7 +52,7 @@ class UISlideOverlays {
companion object {
private const val TAG = "UISlideOverlays";
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View) {
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View): SlideUpMenuOverlay {
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
menu.onOK.subscribe {
@ -54,6 +60,7 @@ class UISlideOverlays {
onOk.invoke();
};
menu.show();
return menu;
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
@ -78,6 +85,7 @@ class UISlideOverlays {
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
}, false),
SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()),
@ -96,7 +104,15 @@ class UISlideOverlays {
}, false) else null,
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
}, false) else null).filterNotNull());
}, false) else null/*,,
SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription",
-1, listOf())
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
showCreateSubscriptionGroup(container, subscription.channel);
}, false)*/
).filterNotNull());
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
@ -134,6 +150,10 @@ class UISlideOverlays {
}
}
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
}
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(LoaderView(container.context))
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
@ -512,6 +532,48 @@ class UISlideOverlays {
return overlay;
}
fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel? = null, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addSubGroupOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_subgroup), container.context.getString(R.string.ok), false, nameInput);
addSubGroupOverlay.onOK.subscribe {
val text = nameInput.text;
if (text.isBlank()) {
return@subscribe;
}
addSubGroupOverlay.hide();
nameInput.deactivate();
nameInput.clear();
if(onCreate == null)
{
//TODO: Do this better, temp
StateApp.instance.contextOrNull?.let {
if(it is MainActivity) {
val subGroup = SubscriptionGroup(text);
if(initialChannel != null) {
subGroup.urls.add(initialChannel.url);
if(initialChannel.thumbnail != null)
subGroup.image = ImageVariable(initialChannel.thumbnail);
}
it.navigate(it.getFragment<SubscriptionGroupFragment>(), subGroup);
}
}
}
else
onCreate(text)
};
addSubGroupOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addSubGroupOverlay.show();
nameInput.activate();
return addSubGroupOverlay
}
fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);

View file

@ -36,6 +36,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage
@ -100,6 +101,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
lateinit var _fragBuy: BuyFragment;
lateinit var _fragSubGroup: SubscriptionGroupFragment;
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
lateinit var _fragBrowser: BrowserFragment;
@ -235,6 +238,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
_fragBuy = BuyFragment.newInstance();
_fragSubGroup = SubscriptionGroupFragment.newInstance();
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
_fragBrowser = BrowserFragment.newInstance();
@ -316,6 +321,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragDownloads.topBar = _fragTopBarGeneral;
_fragImportSubscriptions.topBar = _fragTopBarImport;
_fragImportPlaylists.topBar = _fragTopBarImport;
_fragSubGroup.topBar = _fragTopBarNavigation;
_fragSubGroupList.topBar = _fragTopBarAdd;
_fragBrowser.topBar = _fragTopBarNavigation;
@ -982,6 +989,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
BrowserFragment::class -> _fragBrowser as T;
BuyFragment::class -> _fragBuy as T;
SubscriptionGroupFragment::class -> _fragSubGroup as T;
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
}
}

View file

@ -55,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
Settings.instance.save()
}
val items = Settings.instance.tabs.mapNotNull {
val items = ArrayList(Settings.instance.tabs.mapNotNull {
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
TabViewHolderData(buttonDefinition, it.enabled)
};
});
_listTabs = _recyclerTabs.asAny(items) {
it.onDragDrop.subscribe { vh ->

View file

@ -0,0 +1,14 @@
package com.futo.platformplayer.api.media.models.modifier
class AdhocRequestModifier: IRequestModifier {
val _handler: (String, Map<String,String>)->IRequest;
override var allowByteSkip: Boolean = false;
constructor(modifyReq: (String, Map<String,String>)->IRequest) {
_handler = modifyReq;
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
return _handler(url, headers);
}
}

View file

@ -0,0 +1,6 @@
package com.futo.platformplayer.api.media.models.modifier
interface IModifierOptions {
val applyAuthClient: String?;
val applyCookieClient: String?;
}

View file

@ -0,0 +1,6 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequest {
val url: String?;
val headers: Map<String, String>;
}

View file

@ -0,0 +1,7 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequestModifier {
var allowByteSkip: Boolean;
fun modifyRequest(url: String, headers: Map<String, String>): IRequest
}

View file

@ -20,7 +20,7 @@ class DevJSClient : JSClient {
val devID: String;
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV")), null, script) {
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null, settings: HashMap<String, String?>? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV"), settings), null, script) {
_devScript = script;
_auth = auth;
_captcha = captcha;
@ -49,7 +49,7 @@ class DevJSClient : JSClient {
_auth = auth;
}
fun recreate(context: Context): DevJSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID);
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
}
override fun getCopy(): JSClient {

View file

@ -7,6 +7,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
@ -67,8 +68,8 @@ open class JSClient : IPlatformClient {
var descriptor: SourcePluginDescriptor
private set;
private val _client: JSHttpClient;
private val _clientAuth: JSHttpClient?;
private val _httpClient: JSHttpClient;
private val _httpClientAuth: JSHttpClient?;
private var _searchCapabilities: ResultCapabilities? = null;
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
private var _channelCapabilities: ResultCapabilities? = null;
@ -131,9 +132,9 @@ open class JSClient : IPlatformClient {
_captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray();
_client = JSHttpClient(this, null, _captcha);
_clientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
_httpClient = JSHttpClient(this, null, _captcha);
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
_plugin.withDependency(context, "scripts/polyfil.js");
_plugin.withDependency(context, "scripts/source.js");
@ -160,9 +161,9 @@ open class JSClient : IPlatformClient {
_captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray();
_client = JSHttpClient(this, null, _captcha);
_clientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
_httpClient = JSHttpClient(this, null, _captcha);
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
_plugin.withDependency(context, "scripts/polyfil.js");
_plugin.withDependency(context, "scripts/source.js");
_plugin.withScript(script);
@ -181,6 +182,13 @@ open class JSClient : IPlatformClient {
fun getUnderlyingPlugin(): V8Plugin {
return _plugin;
}
fun getHttpClientById(id: String): JSHttpClient? {
if(_httpClient.clientId == id)
return _httpClient;
if(_httpClientAuth?.clientId == id)
return _httpClientAuth;
return plugin.httpClientOthers[id];
}
override fun initialize() {
Logger.i(TAG, "Plugin [${config.name}] initializing");
@ -254,7 +262,7 @@ open class JSClient : IPlatformClient {
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
return@isBusyWith JSContentPager(config, plugin,
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getHome()"));
}
@ -292,7 +300,7 @@ open class JSClient : IPlatformClient {
@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 {
ensureEnabled();
return@isBusyWith JSContentPager(config, plugin,
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
}
@ -316,7 +324,7 @@ open class JSClient : IPlatformClient {
if(!capabilities.hasSearchChannelContents)
throw IllegalStateException("This plugin does not support channel search");
return@isBusyWith JSContentPager(config, plugin,
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
}
@ -325,7 +333,7 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("query", "Query that channels should match")
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
ensureEnabled();
return@isBusyWith JSChannelPager(config, plugin,
return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
}
@ -372,7 +380,7 @@ open class JSClient : IPlatformClient {
@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 {
ensureEnabled();
return@isBusyWith JSContentPager(config, plugin,
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
}
@ -438,7 +446,7 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("url", "A content url (this platform)")
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
ensureEnabled();
return@isBusyWith IJSContentDetails.fromV8(config,
return@isBusyWith IJSContentDetails.fromV8(this,
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
}
@ -476,13 +484,13 @@ open class JSClient : IPlatformClient {
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
return@isBusyWith EmptyPager<IPlatformComment>();
}
return@isBusyWith JSCommentPager(config, plugin, pager);
return@isBusyWith JSCommentPager(config, this, pager);
}
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
@JSDocsParameter("comment", "Comment object that was returned by getComments")
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
ensureEnabled();
return comment.getReplies(this) ?: JSCommentPager(config, plugin,
return comment.getReplies(this) ?: JSCommentPager(config, this,
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
}
@ -501,7 +509,7 @@ open class JSClient : IPlatformClient {
if(!capabilities.hasGetLiveEvents)
return@isBusyWith null;
ensureEnabled();
return@isBusyWith JSLiveEventPager(config, plugin,
return@isBusyWith JSLiveEventPager(config, this,
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
}
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
@ -514,7 +522,7 @@ open class JSClient : IPlatformClient {
ensureEnabled();
if(!capabilities.hasSearchPlaylists)
throw IllegalStateException("This plugin does not support playlist search");
return@isBusyWith JSContentPager(config, plugin, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
}
@JSOptional
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@ -530,7 +538,7 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("url", "Url of playlist")
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
ensureEnabled();
return@isBusyWith JSPlaylistDetails(plugin, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
}
@JSOptional

View file

@ -26,17 +26,19 @@ class SourcePluginDescriptor {
@kotlinx.serialization.Transient
val onCaptchaChanged = Event0();
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null) {
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, settings: HashMap<String, String?>? = null) {
this.config = config;
this.authEncrypted = authEncrypted;
this.captchaEncrypted = captchaEncrypted;
this.flags = listOf();
this.settings = settings ?: hashMapOf();
}
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) {
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>, settings: HashMap<String, String?>? = null) {
this.config = config;
this.authEncrypted = authEncrypted;
this.captchaEncrypted = captchaEncrypted;
this.flags = flags;
this.settings = settings ?: hashMapOf();
}
fun getSettingsWithDefaults(): HashMap<String, String?> {

View file

@ -1,12 +1,16 @@
package com.futo.platformplayer.api.media.platforms.js.internal
import android.net.Uri
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.matchesDomain
import java.util.UUID
class JSHttpClient : ManagedHttpClient {
private val _jsClient: JSClient?;
@ -14,12 +18,15 @@ class JSHttpClient : ManagedHttpClient {
private val _auth: SourceAuth?;
private val _captcha: SourceCaptchaData?;
val clientId = UUID.randomUUID().toString();
var doUpdateCookies: Boolean = true;
var doApplyCookies: Boolean = true;
var doAllowNewCookies: Boolean = true;
val isLoggedIn: Boolean get() = _auth != null;
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
_jsClient = jsClient;
@ -28,6 +35,7 @@ class JSHttpClient : ManagedHttpClient {
_captcha = captcha;
_currentCookieMap = hashMapOf();
_otherCookieMap = hashMapOf();
if(!auth?.cookieMap.isNullOrEmpty()) {
for(domainCookies in auth!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
@ -49,6 +57,45 @@ class JSHttpClient : ManagedHttpClient {
return newClient;
}
//TODO: Use this in beforeRequest to remove dup code
fun applyHeaders(url: Uri, headers: MutableMap<String, String>, applyAuth: Boolean = false, applyOtherCookies: Boolean = false) {
val domain = url.host!!.lowercase();
val auth = _auth;
if (applyAuth && auth != null) {
//TODO: Possibly add doApplyHeaders
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
headers.put(header.key, header.value);
}
if(doApplyCookies && (applyAuth || applyOtherCookies)) {
val cookiesToApply = hashMapOf<String, String>();
if(applyOtherCookies)
synchronized(_otherCookieMap) {
for(cookie in _otherCookieMap
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
}
if(applyAuth)
synchronized(_currentCookieMap) {
for(cookie in _currentCookieMap
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
};
if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
val existingCookies = headers["Cookie"];
if(!existingCookies.isNullOrEmpty())
headers.put("Cookie", existingCookies.trim(';') + "; " + cookieString);
else
headers.put("Cookie", cookieString);
}
}
}
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
val domain = request.url.host.lowercase();
val auth = _auth;
@ -101,10 +148,10 @@ class JSHttpClient : ManagedHttpClient {
val defaultCookieDomain =
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
for (header in resp.headers) {
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") {
if(header.first.lowercase() == "set-cookie") {
var domainToUse = domain;
val cookie = cookieStringToPair(header.second);
var cookieValue = cookie.second;
var domainToUse = domain;
if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
val cookieParts = cookie.second.split(";");
@ -124,17 +171,33 @@ class JSHttpClient : ManagedHttpClient {
domainToUse = if (cookieVariables.containsKey("domain"))
cookieVariables["domain"]!!.lowercase();
else defaultCookieDomain;
//TODO: Make sure this has no negative effect besides apply cookies to root domain
if(!domainToUse.startsWith("."))
domainToUse = ".${domainToUse}";
}
val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
_currentCookieMap[domainToUse]!!;
else {
val newMap = hashMapOf<String, String>();
_currentCookieMap[domainToUse] = newMap
newMap;
if ((_auth != null || _currentCookieMap.isNotEmpty())) {
val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
_currentCookieMap[domainToUse]!!;
else {
val newMap = hashMapOf<String, String>();
_currentCookieMap[domainToUse] = newMap
newMap;
}
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
cookieMap[cookie.first] = cookieValue;
}
else {
val cookieMap = if (_otherCookieMap.containsKey(domainToUse))
_otherCookieMap[domainToUse]!!;
else {
val newMap = hashMapOf<String, String>();
_otherCookieMap[domainToUse] = newMap
newMap;
}
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
cookieMap[cookie.first] = cookieValue;
}
if(cookieMap.containsKey(cookie.first) || doAllowNewCookies)
cookieMap[cookie.first] = cookieValue;
}
}
}

View file

@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -10,13 +11,14 @@ import com.futo.platformplayer.getOrThrow
interface IJSContent: IPlatformContent {
companion object {
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContent {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
val config = plugin.config;
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
//TODO: Temporary workaround for intercepting details in lists
if(pluginType != null && pluginType.endsWith("Details"))
return IJSContentDetails.fromV8(config, obj);
return IJSContentDetails.fromV8(plugin, obj);
return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideo(config, obj);

View file

@ -4,17 +4,18 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrThrow
interface IJSContentDetails: IPlatformContent {
companion object {
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContentDetails {
val type: Int = obj.getOrThrow(config, "contentType", "ContentDetails");
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(config, obj);
ContentType.POST -> JSPostDetails(config, obj);
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(plugin.config, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}

View file

@ -2,13 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): PlatformAuthorLink {
return PlatformAuthorLink.fromV8(config, obj);

View file

@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
@ -60,6 +61,7 @@ class JSComment : IPlatformComment {
return null;
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
return JSCommentPager(_config!!, _plugin!!, obj);
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return JSCommentPager(_config!!, plugin, obj);
}
}

View file

@ -2,15 +2,16 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
class JSCommentPager : JSPager<IPlatformComment>, IPager<IPlatformComment> {
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) { }
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) { }
override fun convertResult(obj: V8ValueObject): IPlatformComment {
return JSComment(config, plugin, obj);
return JSComment(config, plugin.getUnderlyingPlugin(), obj);
}
}

View file

@ -3,15 +3,16 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.V8Plugin
class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
override val sourceConfig: SourcePluginConfig get() = config;
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformContent {
return IJSContent.fromV8(config, obj);
return IJSContent.fromV8(plugin, obj);
}
}

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPlatformLiveEventPager
import com.futo.platformplayer.engine.V8Plugin
@ -10,7 +11,7 @@ import com.futo.platformplayer.getOrThrow
class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
override var nextRequest: Int;
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
}

View file

@ -4,6 +4,7 @@ import android.os.Looper
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
@ -12,7 +13,7 @@ import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.warnIfMainThread
abstract class JSPager<T> : IPager<T> {
protected val plugin: V8Plugin;
protected val plugin: JSClient;
protected val config: SourcePluginConfig;
protected var pager: V8ValueObject;
@ -21,9 +22,9 @@ abstract class JSPager<T> : IPager<T> {
private var _hasMorePages: Boolean = false;
//private var _morePagesWasFalse: Boolean = false;
val isAvailable get() = plugin._runtime?.let { !it.isClosed && !it.isDead } ?: false;
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) {
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) {
this.plugin = plugin;
this.pager = pager;
this.config = config;
@ -43,7 +44,7 @@ abstract class JSPager<T> : IPager<T> {
override fun nextPage() {
warnIfMainThread("JSPager.nextPage");
pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invoke("nextPage", arrayOf<Any>());
};
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;

View file

@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
@ -13,7 +14,7 @@ import com.futo.platformplayer.models.Playlist
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
override val contents: IPager<IPlatformVideo>;
constructor(plugin: V8Plugin, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
}

View file

@ -2,13 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
class JSPlaylistPager : JSPager<IPlatformPlaylist>, IPager<IPlatformPlaylist> {
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformPlaylist {
return JSPlaylist(config, obj);

View file

@ -54,6 +54,6 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
return JSCommentPager(_pluginConfig, client, commentPager);
}
}

View file

@ -1,18 +1,81 @@
package com.futo.platformplayer.api.media.platforms.js.models
import android.net.Uri
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.IModifierOptions
import com.futo.platformplayer.api.media.models.modifier.IRequest
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.getOrDefault
@kotlinx.serialization.Serializable
class JSRequest : JSRequestModifier.IRequest {
override val url: String;
override val headers: Map<String, String>;
class JSRequest : IRequest {
private val _v8Url: String?;
private val _v8Headers: Map<String, String>?;
private val _v8Options: Options?;
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
override var url: String? = null;
override lateinit var headers: Map<String, String>;
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) {
_v8Url = url;
_v8Headers = headers;
_v8Options = options;
initialize(plugin, originalUrl, originalHeaders);
}
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?) {
val contextName = "ModifyRequestResponse";
url = obj.getOrThrow(config, "url", contextName);
headers = obj.getOrThrow(config, "headers", contextName);
val config = plugin.config;
_v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
Options(config, it);
}
initialize(plugin, originalUrl, originalHeaders);
}
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
val config = plugin.config;
url = _v8Url ?: originalUrl;
headers = _v8Headers ?: originalHeaders ?: mapOf();
if(_v8Options != null) {
if(_v8Options.applyCookieClient != null && url != null) {
val client = plugin.getHttpClientById(_v8Options.applyCookieClient);
if(client != null) {
val toModifyHeaders = headers.toMutableMap();
client.applyHeaders(Uri.parse(url), toModifyHeaders, false, true);
headers = toModifyHeaders;
}
}
if(_v8Options.applyAuthClient != null && url != null) {
val client = plugin.getHttpClientById(_v8Options.applyAuthClient);
if(client != null) {
val toModifyHeaders = headers.toMutableMap();
client.applyHeaders(Uri.parse(url), toModifyHeaders, true, false);
headers = toModifyHeaders;
}
}
}
}
fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest {
return JSRequest(plugin, _v8Url, _v8Headers, _v8Options, originalUrl, originalHeaders);
}
@kotlinx.serialization.Serializable
class Options: IModifierOptions {
override val applyAuthClient: String?;
override val applyCookieClient: String?;
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null);
applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null);
}
}
companion object {
}
}

View file

@ -1,19 +1,28 @@
package com.futo.platformplayer.api.media.platforms.js.models
import android.net.Uri
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.IRequest
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
class JSRequestModifier {
class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient;
private val _config: IV8PluginConfig;
private var _modifier: V8ValueObject;
val allowByteSkip: Boolean;
override var allowByteSkip: Boolean;
constructor(config: IV8PluginConfig, modifier: V8ValueObject) {
constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin;
this._modifier = modifier;
this._config = config;
this._config = plugin.config;
val config = plugin.config;
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
@ -21,22 +30,19 @@ class JSRequestModifier {
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
if (_modifier.isClosed) {
return Request(url, headers);
}
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers);
};
} as V8ValueObject;
return JSRequest(_config, result as V8ValueObject);
val req = JSRequest(_plugin, result, url, headers);
return req;
}
interface IRequest {
val url: String;
val headers: Map<String, String>;
}
data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
}

View file

@ -44,13 +44,14 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
override val subtitles: List<ISubtitleSource>;
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
val contextName = "VideoDetails";
val config = plugin.config;
description = _content.getOrThrow(config, "description", contextName);
video = JSVideoSourceDescriptor.fromV8(config, _content.getOrThrow(config, "video", contextName));
dash = JSSource.fromV8DashNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
hls = JSSource.fromV8HLSNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
live = JSSource.fromV8VideoNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
dash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0));
if(!_content.has("subtitles"))
@ -105,6 +106,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return null;
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
return JSCommentPager(_pluginConfig, client, commentPager);
}
}

View file

@ -2,12 +2,13 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.V8Plugin
class JSVideoPager : JSPager<IPlatformVideo> {
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformVideo {
return JSVideo(config, obj);

View file

@ -2,7 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
@ -19,8 +21,9 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_AUDIOURL, config, obj) {
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
val contextName = "AudioUrlSource";
val config = plugin.config;
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
container = _obj.getOrThrow(config, "container", contextName);

View file

@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
@ -22,8 +24,9 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
&& indexEnd != null)
StreamMetaData(initStart, initEnd, indexStart, indexEnd) else null;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(config, obj) {
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSAudioUrlRangeSource";
val config = plugin.config;
itagId = _obj.getOrDefault(config, "itagId", contextName, null);
initStart = _obj.getOrDefault(config, "initStart", contextName, null);

View file

@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
@ -19,9 +21,9 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource {
override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_DASH, config, obj) {
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
val contextName = "DashSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);
duration = _obj.getOrThrow(config, "duration", contextName);

View file

@ -4,7 +4,9 @@ import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orNull
@ -20,8 +22,9 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_HLS, config, obj) {
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
val contextName = "HLSAudioSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);
@ -33,7 +36,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
companion object {
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(config, obj);
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj);
}
}

View file

@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
@ -19,8 +21,9 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource {
override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(TYPE_HLS, config, obj) {
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
val contextName = "HLSSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);

View file

@ -4,31 +4,47 @@ import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.AdhocRequestModifier
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
abstract class JSSource {
protected val _plugin: JSClient;
protected val _config: IV8PluginConfig;
protected val _obj: V8ValueObject;
private val _hasRequestModifier: Boolean;
val hasRequestModifier: Boolean;
private val _requestModifier: JSRequest?;
val type : String;
constructor(type: String, config: IV8PluginConfig, obj: V8ValueObject) {
this._config = config;
constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
this._plugin = plugin;
this._config = plugin.config;
this._obj = obj;
this.type = type;
_hasRequestModifier = obj.has("getRequestModifier");
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
JSRequest(plugin, it, null, null);
}
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
}
fun getRequestModifier(): JSRequestModifier? {
if (!_hasRequestModifier || _obj.isClosed) {
fun getRequestModifier(): IRequestModifier? {
if(_requestModifier != null)
return AdhocRequestModifier { url, headers ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
};
if (!hasRequestModifier || _obj.isClosed) {
return null;
}
@ -40,16 +56,7 @@ abstract class JSSource {
return null;
}
return JSRequestModifier(_config, result)
}
fun getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier();
return if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else {
DefaultHttpDataSource.Factory();
}
return JSRequestModifier(_plugin, result)
}
companion object {
@ -60,28 +67,28 @@ abstract class JSSource {
const val TYPE_DASH = "DashSource";
const val TYPE_HLS = "HLSSource";
fun fromV8VideoNullable(config: IV8PluginConfig, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(config, it as V8ValueObject) };
fun fromV8Video(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSource {
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
val type = obj.getString("plugin_type");
return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(config, obj);
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(config, obj);
TYPE_HLS -> fromV8HLS(config, obj);
TYPE_DASH -> fromV8Dash(config, obj);
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
TYPE_HLS -> fromV8HLS(plugin, obj);
TYPE_DASH -> fromV8Dash(plugin, obj);
else -> throw NotImplementedError("Unknown type ${type}");
}
}
fun fromV8DashNullable(config: IV8PluginConfig, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(config, it as V8ValueObject) };
fun fromV8Dash(config: IV8PluginConfig, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(config, obj);
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(config, obj);
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
fun fromV8Audio(config: IV8PluginConfig, obj: V8ValueObject) : IAudioSource {
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource {
val type = obj.getString("plugin_type");
return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(config, obj);
TYPE_AUDIOURL -> JSAudioUrlSource(config, obj);
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(config, obj);
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
else -> throw NotImplementedError("Unknown type ${type}");
}
}

View file

@ -5,6 +5,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow
@ -15,15 +16,16 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
override val videoSources: Array<IVideoSource>;
override val audioSources: Array<IAudioSource>;
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
constructor(plugin: JSClient, obj: V8ValueObject) {
this._obj = obj;
val config = plugin.config;
val contextName = "UnMuxVideoSource"
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(config, it as V8ValueObject) }
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.toTypedArray();
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
.map { JSSource.fromV8Audio(config, it as V8ValueObject) }
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
.toTypedArray();
}
}

View file

@ -5,7 +5,9 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
@ -14,12 +16,13 @@ class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
override val isUnMuxed: Boolean;
override val videoSources: Array<IVideoSource>;
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
constructor(plugin: JSClient, obj: V8ValueObject) {
this._obj = obj;
val config = plugin.config;
val contextName = "VideoSourceDescriptor";
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(config, it as V8ValueObject) }
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.toTypedArray();
}
@ -28,11 +31,11 @@ class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
const val TYPE_UNMUXED = "UnMuxVideoSourceDescriptor";
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSourceDescriptor {
fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor {
val type = obj.getString("plugin_type")
return when(type) {
TYPE_MUXED -> JSVideoSourceDescriptor(config, obj);
TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(config, obj);
TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj);
TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(plugin, obj);
else -> throw NotImplementedError("Unknown type: ${type}");
}
}

View file

@ -2,7 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
@ -18,8 +20,9 @@ open class JSVideoUrlSource : IVideoUrlSource, JSSource {
override var priority: Boolean = false;
constructor(config: IV8PluginConfig, obj: V8ValueObject): super(TYPE_VIDEOURL, config, obj) {
constructor(plugin: JSClient, obj: V8ValueObject): super(TYPE_VIDEOURL, plugin, obj) {
val contextName = "JSVideoUrlSource";
val config = plugin.config;
width = _obj.getOrThrow(config, "width", contextName);
height = _obj.getOrThrow(config, "height", contextName);

View file

@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
@ -21,8 +23,9 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
&& indexEnd != null)
StreamMetaData(initStart, initEnd, indexStart, indexEnd) else null;
constructor(config: IV8PluginConfig, obj: V8ValueObject) : super(config, obj) {
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
val contextName = "JSVideoUrlRangeSource";
val config = plugin.config;
itagId = _obj.getOrDefault(config, "itagId", contextName, null);
initStart = _obj.getOrDefault(config, "initStart", contextName, null);

View file

@ -27,10 +27,8 @@ import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.exceptions.DownloadException
import com.futo.platformplayer.hasAnySource
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.isDownloadable
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
@ -38,6 +36,8 @@ import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.toHumanBitrate
import com.futo.platformplayer.toHumanBytesSpeed
import hasAnySource
import isDownloadable
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers

View file

@ -11,6 +11,7 @@ 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.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.exceptions.NoInternetException
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
@ -34,15 +35,24 @@ import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets
import com.futo.platformplayer.warnIfMainThread
import java.util.concurrent.ConcurrentHashMap
class V8Plugin {
val config: IV8PluginConfig;
private val _client: ManagedHttpClient;
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;
fun registerHttpClient(client: JSHttpClient) {
synchronized(_clientOthers) {
_clientOthers.put(client.clientId, client);
}
}
private val _runtimeLock = Object();
var _runtime : V8Runtime? = null;

View file

@ -45,7 +45,12 @@ class PackageHttp: V8Package {
@V8Function
fun newClient(withAuth: Boolean): PackageHttpClient {
return PackageHttpClient(this, if(withAuth) _clientAuth.clone() else _client.clone());
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
if(httpClient is JSHttpClient)
_plugin.registerHttpClient(httpClient);
val client = PackageHttpClient(this, httpClient);
return client;
}
@V8Function
fun getDefaultClient(withAuth: Boolean): PackageHttpClient {
@ -187,10 +192,19 @@ class PackageHttp: V8Package {
@Transient
private val _defaultHeaders = mutableMapOf<String, String>();
@Transient
private val _clientId: String?;
@V8Property
fun clientId(): String? {
return _clientId;
}
constructor(pack: PackageHttp, baseClient: ManagedHttpClient): super() {
_package = pack;
_client = baseClient;
_clientId = if(_client is JSHttpClient) _client.clientId else null;
}
@V8Function

View file

@ -348,6 +348,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>() }),
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
val c = it.context ?: return@ButtonDefinition;
Logger.i(TAG, "settings preventPictureInPicture()");

View file

@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.views.buttons.BigButton
@ -294,7 +295,9 @@ class SourceDetailFragment : MainFragment() {
}
}
val clientIfExists = StatePlugins.instance.getPlugin(config.id);
val clientIfExists = if(config.id != StateDeveloper.DEV_ID)
StatePlugins.instance.getPlugin(config.id);
else null;
groups.add(
BigButtonGroup(c, context.getString(R.string.management),
BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {

View file

@ -0,0 +1,302 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.getSystemService
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.dp
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.SearchView
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
class SubscriptionGroupFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = false;
override val hasBottomBar: Boolean get() = true;
private var _view: SubscriptionGroupView? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
if(parameter is SubscriptionGroup)
_view?.setGroup(StateSubscriptionGroups.instance.getSubscriptionGroup(parameter.id) ?: parameter);
else
_view?.setGroup(null);
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = SubscriptionGroupView(requireContext(), this);
_view = view;
return view;
}
companion object {
private const val TAG = "SourcesFragment";
fun newInstance() = SubscriptionGroupFragment().apply {}
}
private class SubscriptionGroupView: ConstraintLayout {
private val _fragment: SubscriptionGroupFragment;
private val _textGroupTitleContainer: LinearLayout;
private val _textGroupTitle: TextView;
private val _imageGroup: ShapeableImageView;
private val _imageGroupBackground: ImageView;
private val _buttonEditImage: LinearLayout;
private val _searchBar: SearchView;
private val _textGroupMeta: TextView;
private val _buttonSettings: ImageButton;
private val _buttonDelete: ImageButton;
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
private val _disabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
private val _disabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
private val _containerEnabled: LinearLayout;
private val _containerDisabled: LinearLayout;
private val _recyclerCreatorsEnabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
private val _recyclerCreatorsDisabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
private val _overlay: FrameLayout;
private var _group: SubscriptionGroup? = null;
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
inflate(context, R.layout.fragment_subscriptions_group, this);
_fragment = fragment;
_overlay = findViewById(R.id.overlay);
_searchBar = findViewById(R.id.search_bar);
_textGroupTitleContainer = findViewById(R.id.text_group_title_container);
_textGroupTitle = findViewById(R.id.text_group_title);
_imageGroup = findViewById(R.id.image_group);
_imageGroupBackground = findViewById(R.id.group_image_background);
_buttonEditImage = findViewById(R.id.button_edit_image);
_textGroupMeta = findViewById(R.id.text_group_meta);
_buttonSettings = findViewById(R.id.button_settings);
_buttonDelete = findViewById(R.id.button_delete);
_imageGroup.setBackgroundColor(Color.GRAY);
val dp6 = 6.dp(resources);
_imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
_containerEnabled = findViewById(R.id.container_enabled);
_containerDisabled = findViewById(R.id.container_disabled);
_recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) {
it.itemView.setPadding(0, dp6, 0, dp6);
it.onClick.subscribe { channel ->
disableCreator(channel);
};
}
_recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) {
it.itemView.setPadding(0, dp6, 0, dp6);
it.onClick.subscribe { channel ->
enableCreator(channel);
};
}
_recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply {
this.orientation = LinearLayoutManager.VERTICAL;
};
_recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply {
this.orientation = LinearLayoutManager.VERTICAL;
};
_textGroupTitleContainer.setOnClickListener {
_group?.let { editName(it) };
};
_textGroupMeta.setOnClickListener {
_group?.let { editName(it) };
};
_imageGroup.setOnClickListener {
_group?.let { editImage(it) };
};
_buttonEditImage.setOnClickListener {
_group?.let { editImage(it) }
};
_buttonSettings.setOnClickListener {
}
_buttonDelete.setOnClickListener {
_group?.let {
StateSubscriptionGroups.instance.deleteSubscriptionGroup(it.id);
};
fragment.close(true);
}
_buttonSettings.visibility = View.GONE;
_searchBar.onSearchChanged.subscribe {
filterCreators();
}
setGroup(null);
}
fun save() {
_group?.let {
StateSubscriptionGroups.instance.updateSubscriptionGroup(it);
};
}
fun editName(group: SubscriptionGroup) {
val editView = SlideUpMenuTextInput(context, "Group name");
editView.text = group.name;
UISlideOverlays.showOverlay(_overlay, "Edit name", "Save", {
editView.deactivate();
val text = editView.text;
if(!text.isNullOrEmpty()) {
group.name = text;
_textGroupTitle.text = text;
save();
}
}, editView).onCancel.subscribe {
editView.deactivate();
}
editView.activate();
}
fun editImage(group: SubscriptionGroup) {
val overlay = ImageVariableOverlay(context, _enabledCreators.map { it.url });
_overlay.removeAllViews();
_overlay.addView(overlay);
_overlay.alpha = 0f
_overlay.visibility = View.VISIBLE;
_overlay.animate().alpha(1f).setDuration(300).start();
overlay.onSelected.subscribe {
group.image = it;
it.setImageView(_imageGroup);
it.setImageView(_imageGroupBackground);
save();
};
overlay.onClose.subscribe {
_overlay.visibility = View.GONE;
overlay.removeAllViews();
}
}
fun setGroup(group: SubscriptionGroup?) {
_group = group;
_textGroupTitle.text = group?.name;
val image = group?.image;
if(image != null) {
image.setImageView(_imageGroupBackground);
image.setImageView(_imageGroup);
}
else {
_imageGroupBackground.setImageResource(0);
_imageGroup.setImageResource(0);
}
updateMeta();
reloadCreators(group);
}
@SuppressLint("NotifyDataSetChanged")
private fun reloadCreators(group: SubscriptionGroup?) {
_enabledCreators.clear();
_disabledCreators.clear();
if(group != null) {
val urls = group.urls.toList();
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
_disabledCreators.addAll(subs.filter { !urls.contains(it.url) });
}
filterCreators();
}
private fun filterCreators() {
val query = _searchBar.textSearch.text.toString().lowercase();
val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) };
val filteredDisabled = _disabledCreators.filter { it.name.lowercase().contains(query) };
//Optimize
_enabledCreatorsFiltered.clear();
_enabledCreatorsFiltered.addAll(filteredEnabled);
_disabledCreatorsFiltered.clear();
_disabledCreatorsFiltered.addAll(filteredDisabled);
_recyclerCreatorsEnabled.notifyContentChanged();
_recyclerCreatorsDisabled.notifyContentChanged();
}
private fun enableCreator(channel: IPlatformChannel) {
val index = _disabledCreatorsFiltered.indexOf(channel);
if (index >= 0) {
_disabledCreators.remove(channel)
_disabledCreatorsFiltered.remove(channel);
_recyclerCreatorsDisabled.adapter.notifyItemRangeRemoved(index);
_enabledCreators.add(channel);
_enabledCreatorsFiltered.add(channel);
_recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreatorsFiltered.size - 1);
_group?.let {
if(!it.urls.contains(channel.url)) {
it.urls.add(channel.url);
save();
}
}
updateMeta();
}
}
private fun disableCreator(channel: IPlatformChannel) {
val index = _enabledCreatorsFiltered.indexOf(channel);
if (index >= 0) {
_enabledCreators.remove(channel)
_enabledCreatorsFiltered.removeAt(index);
_recyclerCreatorsEnabled.adapter.notifyItemRangeRemoved(index);
_disabledCreators.add(channel);
_disabledCreatorsFiltered.add(channel);
_recyclerCreatorsDisabled.adapter.notifyItemInserted(_disabledCreatorsFiltered.size - 1);
_group?.let {
it.urls.remove(channel.url);
save();
}
updateMeta();
}
}
private fun updateMeta() {
_textGroupMeta.text = "${_enabledCreators.size} creators";
}
}
}

View file

@ -0,0 +1,141 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.activities.AddSourceOptionsActivity
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.ItemMoveCallback
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupListViewHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Collections
class SubscriptionGroupListFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _touchHelper: ItemTouchHelper? = null;
private var _subs: ArrayList<SubscriptionGroup> = arrayListOf();
private var _list: AnyAdapterView<SubscriptionGroup, SubscriptionGroupListViewHolder>? = null;
private var _overlay: FrameLayout? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_subscriptions_group_list, container, false);
_overlay = view.findViewById(R.id.overlay);
val recycler = view.findViewById<RecyclerView>(R.id.list);
val callback = ItemMoveCallback();
_touchHelper = ItemTouchHelper(callback);
_touchHelper?.attachToRecyclerView(recycler);
_subs.clear();
_subs.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups().sortedBy { it.priority });
_list = recycler.asAny(_subs, RecyclerView.VERTICAL){
it.onClick.subscribe {
navigate<SubscriptionGroupFragment>(it);
};
it.onSettings.subscribe {
};
it.onDelete.subscribe { group ->
val loc = _subs.indexOf(group);
_subs.remove(group);
_list?.adapter?.notifyItemRangeRemoved(loc);
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id);
};
it.onDragDrop.subscribe {
_touchHelper?.startDrag(it);
};
};
callback.onRowMoved.subscribe(::groupMoved);
return view;
}
private fun groupMoved(fromPosition: Int, toPosition: Int) {
Logger.i("SubscriptionGroupListFragment", "Moved ${fromPosition} to ${toPosition}");
synchronized(_subs) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(_subs, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(_subs, i, i - 1)
}
}
}
_list?.adapter?.notifyItemMoved(fromPosition, toPosition);
synchronized(_subs) {
for(i in 0 until _subs.size) {
val sub = _subs[i];
if(sub.priority != i) {
sub.priority = i;
StateSubscriptionGroups.instance.updateSubscriptionGroup(sub, true);
}
}
}
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
updateGroups();
StateSubscriptionGroups.instance.onGroupsChanged.subscribe(this) {
updateGroups();
}
if(topBar is AddTopBarFragment)
(topBar as AddTopBarFragment).onAdd.subscribe {
_overlay?.let {
UISlideOverlays.showCreateSubscriptionGroup(it)
}
};
}
private fun updateGroups() {
lifecycleScope.launch(Dispatchers.Main) {
_subs.clear();
_subs.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups().sortedBy { it.priority });
_list?.adapter?.notifyContentChanged();
}
}
override fun onHide() {
super.onHide();
StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
if(topBar is AddTopBarFragment)
(topBar as AddTopBarFragment).onAdd.remove(this);
}
override fun onBackPressed(): Boolean {
return false;
}
companion object {
fun newInstance() = SubscriptionGroupListFragment().apply {}
}
}

View file

@ -21,6 +21,7 @@ import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.exceptions.RateLimitException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform
@ -99,6 +100,8 @@ class SubscriptionsFeedFragment : MainFragment() {
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
private var _subGroup: SubscriptionGroup? = null;
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
@ -254,6 +257,18 @@ class SubscriptionsFeedFragment : MainFragment() {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
};
_subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
_subscriptionBar?.onToggleGroup?.subscribe { g ->
if(g is SubscriptionGroup.Add)
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
else {
_subGroup = g;
loadCache(); //TODO: Proper subset update
}
};
_subscriptionBar?.onHoldGroup?.subscribe { g ->
if(g !is SubscriptionGroup.Add)
fragment.navigate<SubscriptionGroupFragment>(g);
};
synchronized(_filterLock) {
_subscriptionBar?.setToggles(
@ -288,9 +303,15 @@ class SubscriptionsFeedFragment : MainFragment() {
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
val nowSoon = OffsetDateTime.now().plusMinutes(5);
val filterGroup = _subGroup;
return results.filter {
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
//TODO: Check against a sub cache
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
return@filter false;
if(it.datetime?.isAfter(nowSoon) == true) {
if(!_filterSettings.allowPlanned)
return@filter false;

View file

@ -22,6 +22,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlR
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.Language
import getHttpDataSourceFactory
import kotlin.math.abs
class VideoHelper {

View file

@ -1,14 +1,26 @@
package com.futo.platformplayer.models
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.futo.platformplayer.PresetImages
import com.futo.platformplayer.R
import kotlinx.serialization.Contextual
import kotlinx.serialization.Transient
import java.io.File
data class ImageVariable(val url: String? = null, val resId: Int? = null, val bitmap: Bitmap? = null) {
@kotlinx.serialization.Serializable
data class ImageVariable(
val url: String? = null,
val resId: Int? = null,
@Transient
@Contextual
private val bitmap: Bitmap? = null,
val presetName: String? = null) {
@SuppressLint("DiscouragedApi")
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
if(bitmap != null) {
Glide.with(imageView)
@ -23,6 +35,9 @@ data class ImageVariable(val url: String? = null, val resId: Int? = null, val bi
.load(url)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(imageView);
} else if(!presetName.isNullOrEmpty()) {
val resId = PresetImages.getPresetResIdByName(presetName);
imageView.setImageResource(resId);
} else if (fallbackResId != -1) {
Glide.with(imageView)
.load(fallbackResId)
@ -44,6 +59,9 @@ data class ImageVariable(val url: String? = null, val resId: Int? = null, val bi
fun fromBitmap(bitmap: Bitmap): ImageVariable {
return ImageVariable(null, null, bitmap);
}
fun fromPresetName(str: String): ImageVariable {
return ImageVariable(null, null, null, str);
}
fun fromFile(file: File): ImageVariable {
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
}

View file

@ -0,0 +1,33 @@
package com.futo.platformplayer.models
import java.util.UUID
@kotlinx.serialization.Serializable
open class SubscriptionGroup {
var id: String = UUID.randomUUID().toString();
var name: String;
var image: ImageVariable? = null;
var urls: MutableList<String> = mutableListOf();
var priority: Int = 99;
constructor(name: String) {
this.name = name;
}
constructor(parent: SubscriptionGroup) {
this.id = parent.id;
this.name = parent.name;
this.image = parent.image;
this.urls = parent.urls;
this.priority = parent.priority;
}
class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) {
var selected: Boolean = isSelected;
}
class Add: SubscriptionGroup("+") {
init {
urls.add("+");
}
}
}

View file

@ -133,6 +133,7 @@ class StateApp {
//Files
private var _tempDirectory: File? = null;
private var _persistentDirectory: File? = null;
//AutoRotate
@ -165,6 +166,16 @@ class StateApp {
return File(_tempDirectory, name);
}
fun getPersistFile(extension: String? = null): File {
val name = UUID.randomUUID().toString() +
if(extension != null)
".${extension}"
else
"";
return File(_persistentDirectory, name);
}
fun getCurrentSystemAutoRotate(): Boolean {
_context?.let {
systemAutoRotate = android.provider.Settings.System.getInt(
@ -290,6 +301,10 @@ class StateApp {
_tempDirectory?.deleteRecursively();
}
_tempDirectory?.mkdirs();
_persistentDirectory = File(context.filesDir, "persist");
if(_persistentDirectory?.exists() == false) {
_persistentDirectory?.mkdirs();
}
}
}

View file

@ -1,5 +1,6 @@
package com.futo.platformplayer.states
import android.content.Context
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.server.ManagedHttpServer
import com.futo.platformplayer.developer.DeveloperEndpoints
@ -93,6 +94,13 @@ class StateDeveloper {
}
}
fun setDevClientSettings(settings: HashMap<String, String?>) {
val client = StatePlatform.instance.getDevClient();
client?.let {
it.descriptor.settings = settings;
};
}
fun runServer() {
if(_server != null)

View file

@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.developer.DeveloperEndpoints
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.stores.FragmentedStorage
@ -411,6 +412,16 @@ class StatePlugins {
fun setPluginSettings(id: String, map: Map<String, String?>) {
val newSettings = HashMap(map);
if(id == StateDeveloper.DEV_ID)
{
val decConfig = StatePlatform.instance.getDevClient()?.config ?: return;
for(setting in decConfig.settings) {
if(!newSettings.containsKey(setting.variableOrName) || newSettings[setting.variableOrName] == null)
newSettings[setting.variableOrName] = setting.default;
}
StateDeveloper.instance.setDevClientSettings(newSettings);
return;
}
val plugin = getPlugin(id);
if(plugin != null) {

View file

@ -0,0 +1,94 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
import kotlinx.coroutines.*
import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
import kotlin.collections.ArrayList
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.streams.asSequence
import kotlin.streams.toList
import kotlin.system.measureTimeMillis
/***
* Used to maintain subscription groups
*/
class StateSubscriptionGroups {
private val _subGroups = FragmentedStorage.storeJson<SubscriptionGroup>("subscription_groups")
.withUnique { it.id }
.load();
val onGroupsChanged = Event0();
fun getSubscriptionGroup(id: String): SubscriptionGroup? {
return _subGroups.findItem { it.id == id };
}
fun getSubscriptionGroups(): List<SubscriptionGroup> {
return _subGroups.getItems();
}
fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false) {
_subGroups.save(subGroup);
if(!preventNotify)
onGroupsChanged.emit();
}
fun deleteSubscriptionGroup(id: String){
val group = getSubscriptionGroup(id);
if(group != null) {
_subGroups.delete(group);
onGroupsChanged.emit();
}
}
companion object {
const val TAG = "StateSubscriptionGroups";
const val VERSION = 1;
private var _instance : StateSubscriptionGroups? = null;
val instance : StateSubscriptionGroups
get(){
if(_instance == null)
_instance = StateSubscriptionGroups();
return _instance!!;
};
fun finish() {
_instance?.let {
_instance = null;
}
}
}
}

View file

@ -46,9 +46,10 @@ class AnyAdapterView<I, T>(view: RecyclerView, adapter: BaseAnyAdapter<I, T, T>,
where T : AnyAdapter.AnyViewHolder<I>{
companion object {
/*
inline fun <I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAny(list: List<I>, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView<I, T> {
return asAny(ArrayList(list), orientation, reversed, onCreate);
}
}*/
inline fun <I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAny(list: ArrayList<I>, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView<I, T> {
return AnyAdapterView(this, AnyAdapter.create(list, onCreate), orientation, reversed);
}

View file

@ -0,0 +1,33 @@
package com.futo.platformplayer.views
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
class SearchView : FrameLayout {
val textSearch: TextView;
val buttonClear: ImageButton;
var onSearchChanged = Event1<String>();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_search_bar, this);
textSearch = findViewById(R.id.edit_search)
buttonClear = findViewById(R.id.button_clear_search)
buttonClear.setOnClickListener { textSearch.text = "" };
textSearch.addTextChangedListener {
onSearchChanged.emit(it.toString());
};
}
}

View file

@ -1,7 +1,11 @@
package com.futo.platformplayer.views.adapters
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import java.lang.reflect.Constructor
@ -47,6 +51,7 @@ open class BaseAnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>, IT : ViewHolder> {
cb(item);
}
@SuppressLint("NotifyDataSetChanged")
fun notifyContentChanged() {
adapter.notifyDataSetChanged();
}
@ -116,7 +121,6 @@ class AnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>> : BaseAnyAdapter<I, T, T> {
private class Adapter<I, T : AnyViewHolder<I>> : RecyclerView.Adapter<T> {
private val _parent: AnyAdapter<I, T>;
constructor(parentAdapter: AnyAdapter<I, T>) {
_parent = parentAdapter;
}

View file

@ -0,0 +1,164 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<IPlatformChannel>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_bar_icon, _viewGroup, false)) {
private val _creatorThumbnail: CreatorThumbnail;
private val _name: TextView;
private var _channel: IPlatformChannel? = null;
val onClick = Event1<IPlatformChannel>();
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
init {
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
_name = _view.findViewById(R.id.text_channel_name);
_view.findViewById<LinearLayout>(R.id.root).setOnClickListener {
val s = _channel ?: return@setOnClickListener;
onClick.emit(s);
}
}
override fun bind(value: IPlatformChannel) {
_taskLoadProfile.cancel();
_channel = value;
_creatorThumbnail.setThumbnail(value.thumbnail, false);
_name.text = value.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(value.id);
}
} else {
_taskLoadProfile.run(value.id);
}
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_55 = 55.dp(itemView.context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
_name.text = profile.systemState.username;
}
}
companion object {
private const val TAG = "CreatorBarViewHolder";
}
}
class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SelectableCreatorBarViewHolder.Selectable>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_bar_icon, _viewGroup, false)) {
private val _creatorThumbnail: CreatorThumbnail;
private val _name: TextView;
private var _channel: Selectable? = null;
val onClick = Event1<Selectable>();
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
init {
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
_name = _view.findViewById(R.id.text_channel_name);
_view.findViewById<LinearLayout>(R.id.root).setOnClickListener {
val s = _channel ?: return@setOnClickListener;
onClick.emit(s);
}
}
override fun bind(value: Selectable) {
_taskLoadProfile.cancel();
_channel = value;
if(value.active)
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimaryDark, null))
else
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null))
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
_name.text = value.channel.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(value.channel.id);
}
} else {
_taskLoadProfile.run(value.channel.id);
}
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_55 = 55.dp(itemView.context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
if (profile != null) {
_name.text = profile.systemState.username;
}
}
companion object {
private const val TAG = "CreatorBarViewHolder";
}
data class Selectable(var channel: IPlatformChannel, var active: Boolean)
}

View file

@ -0,0 +1,80 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
class SubscriptionGroupBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_group_bar, _viewGroup, false)) {
private var _group: SubscriptionGroup? = null;
private val _image: ShapeableImageView;
private val _textSubGroup: TextView;
val onClick = Event1<SubscriptionGroup>();
val onClickLong = Event1<SubscriptionGroup>();
init {
_image = _view.findViewById(R.id.image);
_textSubGroup = _view.findViewById(R.id.text_sub_group);
val dp6 = 6.dp(_view.resources);
_image.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
_view.setOnClickListener {
_group?.let {
onClick.emit(it);
}
}
_view.setOnLongClickListener {
_group?.let {
onClickLong.emit(it);
}
true;
}
}
override fun bind(value: SubscriptionGroup) {
_group = value;
val img = value.image;
if(img != null)
img.setImageView(_image)
else {
_image.setImageResource(0);
if(value is SubscriptionGroup.Add)
_image.setBackgroundColor(Color.DKGRAY);
}
_textSubGroup.text = value.name;
if(value is SubscriptionGroup.Selectable && value.selected)
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
else
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
}
companion object {
private const val TAG = "SubscriptionGroupBarViewHolder";
}
}

View file

@ -0,0 +1,109 @@
package com.futo.platformplayer.views.adapters.viewholders
import android.graphics.Color
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.ItemMoveCallback
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
class SubscriptionGroupListViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SubscriptionGroup>(
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_subscription_group, _viewGroup, false)) {
private var _group: SubscriptionGroup? = null;
private val _thumb: ImageView;
private val _image: ShapeableImageView;
private val _textSubGroup: TextView;
private val _textSubGroupMeta: TextView;
private val _buttonSettings: ImageButton;
private val _buttonDelete: ImageButton;
val onClick = Event1<SubscriptionGroup>();
val onSettings = Event1<SubscriptionGroup>();
val onDelete = Event1<SubscriptionGroup>();
val onDragDrop = Event1<RecyclerView.ViewHolder>();
init {
_thumb = _view.findViewById(R.id.thumb);
_image = _view.findViewById(R.id.image);
_textSubGroup = _view.findViewById(R.id.text_sub_group);
_textSubGroupMeta = _view.findViewById(R.id.text_sub_group_meta);
_buttonSettings = _view.findViewById(R.id.button_settings);
_buttonDelete = _view.findViewById(R.id.button_trash);
val dp6 = 6.dp(_view.resources);
_image.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
_view.setOnClickListener {
_group?.let {
onClick.emit(it);
}
}
_thumb.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
onDragDrop.emit(this);
}
false
};
_buttonSettings.setOnClickListener {
_group?.let {
onSettings.emit(it);
};
}
_buttonDelete.setOnClickListener {
_group?.let {
onDelete.emit(it);
};
}
}
override fun bind(value: SubscriptionGroup) {
_group = value;
val img = value.image;
if(img != null)
img.setImageView(_image)
else {
_image.setImageResource(0);
if(value is SubscriptionGroup.Add)
_image.setBackgroundColor(Color.DKGRAY);
}
_textSubGroup.text = value.name;
_textSubGroupMeta.text = "${value.urls.size} subscriptions";
if(value is SubscriptionGroup.Selectable && value.selected)
_view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null));
else
_view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null));
}
companion object {
private const val TAG = "SubscriptionGroupBarViewHolder";
}
}

View file

@ -0,0 +1,234 @@
package com.futo.platformplayer.views.overlays
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.shapes.Shape
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.PresetImages
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.AnyAdapter
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
import com.futo.platformplayer.views.adapters.viewholders.SelectableCreatorBarViewHolder
import com.futo.platformplayer.views.buttons.BigButton
import com.github.dhaval2404.imagepicker.ImagePicker
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
import java.io.File
class ImageVariableOverlay: ConstraintLayout {
private val _buttonGallery: BigButton;
private val _imageGallerySelected: ImageView;
private val _imageGallerySelectedContainer: LinearLayout;
private val _buttonSelect: Button;
private val _topbar: OverlayTopbar;
private val _recyclerPresets: AnyAdapterView<PresetImage, PresetViewHolder>;
private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>;
private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf();
private val _presets: ArrayList<PresetImage> =
ArrayList(PresetImages.images.map { PresetImage(it.value, it.key, false) });
private var _selected: ImageVariable? = null;
private var _selectedFile: String? = null;
val onSelected = Event1<ImageVariable>();
val onClose = Event0();
constructor(context: Context, creatorFilters: List<String>? = null): super(context) {
val subs = StateSubscriptions.instance.getSubscriptions();
if(creatorFilters != null) {
_creators.addAll(subs
.filter { creatorFilters.contains(it.channel.url) }
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
}
else
_creators.addAll(subs
.map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) });
_recyclerCreators.notifyContentChanged();
}
constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { }
init {
inflate(context, R.layout.overlay_image_variable, this);
_topbar = findViewById(R.id.topbar);
_buttonGallery = findViewById(R.id.button_gallery);
_imageGallerySelected = findViewById(R.id.gallery_selected);
_imageGallerySelectedContainer = findViewById(R.id.gallery_selected_container);
_buttonSelect = findViewById(R.id.button_select);
_recyclerPresets = findViewById<RecyclerView>(R.id.recycler_presets).asAny(_presets, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
_selected = ImageVariable.fromPresetName(it.name);
updateSelected();
};
};
val dp6 = 6.dp(resources);
_recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView ->
creatorView.itemView.setPadding(0, dp6, 0, dp6);
creatorView.onClick.subscribe {
if(it.channel.thumbnail == null) {
UIDialogs.toast(context, "No thumbnail found");
return@subscribe;
}
_selected = ImageVariable(it.channel.thumbnail);
updateSelected();
};
};
_recyclerCreators.view.layoutManager = GridLayoutManager(context, 5).apply {
this.orientation = LinearLayoutManager.VERTICAL;
};
_buttonGallery.onClick.subscribe {
val context = StateApp.instance.contextOrNull;
if(context is IWithResultLauncher && context is MainActivity) {
ImagePicker.with(context)
.compress(512)
.maxResultSize(750, 500)
.createIntent {
context.launchForResult(it, 888) {
if(it.resultCode == Activity.RESULT_OK) {
cleanupLastFile();
val fileUri = it.data?.data;
if(fileUri != null) {
val file = fileUri.toFile();
val ext = file.extension;
val persistFile = StateApp.instance.getPersistFile(ext);
file.copyTo(persistFile);
_selectedFile = persistFile.toUri().toString();
_selected = ImageVariable(_selectedFile);
updateSelected();
}
}
};
};
}
};
_imageGallerySelectedContainer.setOnClickListener {
if(_selectedFile != null) {
_selected = ImageVariable(_selectedFile);
updateSelected();
}
}
_buttonSelect.setOnClickListener {
_selected?.let {
select(it);
}
};
_topbar.onClose.subscribe {
onClose.emit();
}
updateSelected();
}
fun updateSelected() {
val id = _selected?.resId;
val name = _selected?.presetName;
val url = _selected?.url;
_presets.forEach { p -> p.active = p.name == name };
_recyclerPresets.notifyContentChanged();
_creators.forEach { p -> p.active = p.channel.thumbnail == url };
_recyclerCreators.notifyContentChanged();
if(_selectedFile != null) {
_imageGallerySelectedContainer.visibility = View.VISIBLE;
Glide.with(_imageGallerySelected)
.load(_selectedFile)
.into(_imageGallerySelected);
}
else
_imageGallerySelectedContainer.visibility = View.GONE;
if(_selected?.url == _selectedFile)
_imageGallerySelectedContainer.setBackgroundColor(resources.getColor(R.color.colorPrimary, null));
else
_imageGallerySelectedContainer.setBackgroundColor(resources.getColor(R.color.transparent, null));
if(_selected != null)
_buttonSelect.alpha = 1f;
else
_buttonSelect.alpha = 0.5f;
}
fun cleanupLastFile() {
_selectedFile?.let {
val file = File(it);
if(file.exists())
file.delete();
_selectedFile = null;
}
}
fun select(variable: ImageVariable) {
if(_selected?.url != _selectedFile)
cleanupLastFile();
onSelected.emit(variable);
onClose.emit();
}
class PresetViewHolder(viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<PresetImage>(LinearLayout(viewGroup.context)) {
private val view = _view as LinearLayout;
private val imageView = ShapeableImageView(viewGroup.context);
private var value: PresetImage = PresetImage(0, "", false);
val onClick = Event1<PresetImage>();
init {
view.addView(imageView);
val dp2 = 2.dp(viewGroup.context.resources);
val dp6 = 6.dp(viewGroup.context.resources);
view.setPadding(dp2, dp2, dp2, dp2);
imageView.setOnClickListener {
onClick.emit(value);
}
imageView.layoutParams = LinearLayout.LayoutParams(110.dp(viewGroup.context.resources), 70.dp(viewGroup.context.resources)).apply {
//this.rightMargin = dp6
}
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
imageView.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
.build()
}
override fun bind(value: PresetImage) {
imageView.setImageResource(value.id);
this.value = value;
setActive(value.active);
}
fun setActive(active: Boolean) {
if(active)
_view.setBackgroundColor(view.context.resources.getColor(R.color.colorPrimary, null));
else
_view.setBackgroundColor(view.context.resources.getColor(R.color.transparent, null));
}
}
data class PresetImage(var id: Int, var name: String, var active: Boolean);
}

View file

@ -18,7 +18,8 @@ class SlideUpMenuTextInput : LinearLayout {
private lateinit var _editText: EditText;
private lateinit var _inputMethodManager: InputMethodManager;
val text: String get() = _editText.text.toString();
var text: String get() = _editText.text.toString()
set(v: String) = _editText.setText(v);
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
init();

View file

@ -3,37 +3,113 @@ package com.futo.platformplayer.views.subscriptions
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.others.ToggleTagView
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionBarViewHolder
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SubscriptionBar : LinearLayout {
private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>
private val _tagsContainer: LinearLayout;
private val _groups: ArrayList<SubscriptionGroup>;
private var _group: SubscriptionGroup? = null;
val onClickChannel = Event1<SerializedChannel>();
val onToggleGroup = Event1<SubscriptionGroup?>();
val onHoldGroup = Event1<SubscriptionGroup>();
override fun onAttachedToWindow() {
super.onAttachedToWindow();
StateSubscriptionGroups.instance.onGroupsChanged.subscribe(this) {
findViewTreeLifecycleOwner()?.lifecycleScope?.launch(Dispatchers.Main) {
reloadGroups();
}
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow();
StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
}
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_subscription_bar, this);
val subscriptions = StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds };
val subscriptions = ArrayList(StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds });
_adapterView = findViewById<RecyclerView>(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) {
it.onClick.subscribe { c ->
onClickChannel.emit(c.channel);
};
};
_groups = ArrayList(getGroups());
_subGroups = findViewById<RecyclerView>(R.id.recycler_subgroups).asAny(_groups, orientation = RecyclerView.HORIZONTAL) {
it.onClick.subscribe(::groupClicked);
it.onClickLong.subscribe { g ->
onHoldGroup.emit(g);
}
}
_tagsContainer = findViewById(R.id.container_tags);
}
private fun groupClicked(g: SubscriptionGroup) {
if(g is SubscriptionGroup.Add) {
onToggleGroup.emit(g);
return;
}
val isSame = _group == g;
_group?.let {
if (it is SubscriptionGroup.Selectable) {
it.selected = false;
val index = _groups.indexOf(it);
if (index >= 0)
_subGroups.notifyContentChanged(index);
}
}
if(isSame) {
_group = null;
onToggleGroup.emit(null);
}
else {
_group = g;
if(g is SubscriptionGroup.Selectable)
g.selected = true;
_subGroups.notifyContentChanged(_groups.indexOf(g));
onToggleGroup.emit(g);
}
}
private fun reloadGroups() {
val results = getGroups();
_groups.clear();
_groups.addAll(results);
_subGroups.notifyContentChanged();
}
private fun getGroups(): List<SubscriptionGroup> {
return if(Settings.instance.subscriptions.showSubscriptionGroups)
(StateSubscriptionGroups.instance.getSubscriptionGroups()
.sortedBy { it.priority }
.map { SubscriptionGroup.Selectable(it, it.id == _group?.id) } +
listOf(SubscriptionGroup.Add()));
else listOf();
}
fun setToggles(vararg buttons: Toggle) {
_tagsContainer.removeAllViews();

View file

@ -38,12 +38,14 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.video.PlayerManager
import getHttpDataSourceFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -402,22 +404,31 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
Logger.i(TAG, "Loading VideoSource [Url]");
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT))
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
videoSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource)
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
Logger.i(TAG, "Loading VideoSource [Dash]");
_lastVideoMediaSource = DashMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT))
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
videoSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource(MediaItem.fromUri(videoSource.url))
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
Logger.i(TAG, "Loading VideoSource [HLS]");
_lastVideoMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT))
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
videoSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastVideoMediaSource = HlsMediaSource.Factory(dataSource)
.createMediaSource(MediaItem.fromUri(videoSource.url));
}
@ -455,15 +466,21 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
@OptIn(UnstableApi::class)
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
Logger.i(TAG, "Loading AudioSource [Url]");
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT))
val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
audioSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
}
@OptIn(UnstableApi::class)
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
Logger.i(TAG, "Loading AudioSource [HLS]");
_lastAudioMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT))
val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
audioSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastAudioMediaSource = HlsMediaSource.Factory(dataSource)
.createMediaSource(MediaItem.fromUri(audioSource.url));
}

View file

@ -11,6 +11,8 @@ import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.futo.platformplayer.api.media.models.modifier.IRequest;
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier;
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
import androidx.media3.common.C;
import androidx.media3.common.PlaybackException;
@ -60,7 +62,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
private int readTimeoutMs;
private boolean allowCrossProtocolRedirects;
private boolean keepPostFor302Redirects;
@Nullable private JSRequestModifier requestModifier = null;
@Nullable private IRequestModifier requestModifier = null;
/** Creates an instance. */
public Factory() {
@ -83,7 +85,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param requestModifier The request modifier that will be used, or {@code null} to use no request modifier
* @return This factory.
*/
public Factory setRequestModifier(@Nullable JSRequestModifier requestModifier) {
public Factory setRequestModifier(@Nullable IRequestModifier requestModifier) {
this.requestModifier = requestModifier;
return this;
}
@ -228,7 +230,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
private int responseCode;
private long bytesToRead;
private long bytesRead;
@Nullable private JSRequestModifier requestModifier;
@Nullable private IRequestModifier requestModifier;
private JSHttpDataSource(
@Nullable String userAgent,
@ -238,7 +240,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
@Nullable RequestProperties defaultRequestProperties,
@Nullable Predicate<String> contentTypePredicate,
boolean keepPostFor302Redirects,
@Nullable JSRequestModifier requestModifier) {
@Nullable IRequestModifier requestModifier) {
super(/* isNetwork= */ true);
this.userAgent = userAgent;
this.connectTimeoutMillis = connectTimeoutMillis;
@ -574,8 +576,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
String requestUrl = url.toString();
if (requestModifier != null) {
JSRequestModifier.IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders);
requestUrl = result.getUrl();
IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders);
String modifiedUrl = result.getUrl();
requestUrl = (modifiedUrl != null) ? modifiedUrl : requestUrl;
requestHeaders = result.getHeaders();
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorPrimary" />
<corners android:radius="1dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M164.62,720Q137.96,720 118.98,701.02Q100,682.04 100,655.39L100,304.61Q100,277.96 118.98,258.98Q137.96,240 164.62,240L515.39,240Q542.04,240 561.02,258.98Q580,277.96 580,304.61L580,655.39Q580,682.04 561.02,701.02Q542.04,720 515.39,720L164.62,720ZM692.69,440Q678.39,440 669.19,430.81Q660,421.61 660,407.31L660,272.69Q660,258.38 669.19,249.19Q678.39,240 692.69,240L827.31,240Q841.62,240 850.81,249.19Q860,258.38 860,272.69L860,407.31Q860,421.61 850.81,430.81Q841.62,440 827.31,440L692.69,440ZM700,400L820,400L820,280L700,280L700,400ZM164.62,680L515.39,680Q526.15,680 533.08,673.08Q540,666.15 540,655.39L540,304.61Q540,293.85 533.08,286.92Q526.15,280 515.39,280L164.62,280Q153.85,280 146.92,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 146.92,673.08Q153.85,680 164.62,680ZM187.69,596.15L492.31,596.15L395,466.15L320,566.15L265,493.15L187.69,596.15ZM692.69,720Q678.39,720 669.19,710.81Q660,701.62 660,687.31L660,552.69Q660,538.39 669.19,529.19Q678.39,520 692.69,520L827.31,520Q841.62,520 850.81,529.19Q860,538.39 860,552.69L860,687.31Q860,701.62 850.81,710.81Q841.62,720 827.31,720L692.69,720ZM700,680L820,680L820,560L700,560L700,680ZM140,680Q140,680 140,673.08Q140,666.15 140,655.39L140,304.61Q140,293.85 140,286.92Q140,280 140,280L140,280Q140,280 140,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 140,673.08Q140,680 140,680L140,680ZM700,400L700,280L700,280L700,400L700,400ZM700,680L700,560L700,560L700,680L700,680Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

View file

@ -0,0 +1,235 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:animateLayoutChanges="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#AAAAAA">
<ImageView
android:id="@+id/group_image_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/xp_book"
android:scaleType="centerCrop" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#AA000000">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:orientation="horizontal">
<ImageButton
android:id="@+id/button_delete"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="0dp"
android:src="@drawable/ic_trash"
app:tint="#CC0000"
android:padding="10dp"
android:background="@color/transparent"
android:visibility="visible"
android:scaleType="fitCenter" />
<ImageButton
android:id="@+id/button_settings"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:src="@drawable/ic_settings"
android:padding="10dp"
android:background="@color/transparent"
android:visibility="visible"
android:scaleType="fitCenter" />
</LinearLayout>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_group"
android:layout_width="110dp"
android:layout_height="70dp"
android:adjustViewBounds="true"
app:circularflow_defaultRadius="10dp"
android:layout_marginLeft="30dp"
android:src="@drawable/xp_book"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" />
<LinearLayout
android:id="@+id/button_edit_image"
android:layout_width="30dp"
android:layout_height="30dp"
app:layout_constraintBottom_toTopOf="@id/image_group"
app:layout_constraintLeft_toRightOf="@id/image_group"
android:layout_marginLeft="-15dp"
android:layout_marginBottom="-15dp"
android:background="@drawable/background_pill">
<ImageButton
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
android:clickable="false"
android:scaleType="fitCenter"
android:background="@color/transparent"
android:src="@drawable/ic_edit"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@id/image_group"
app:layout_constraintTop_toTopOf="@id/image_group"
app:layout_constraintBottom_toBottomOf="@id/image_group"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginStart="25dp"
android:layout_marginTop="10dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/text_group_title_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_group_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_bold"
android:textSize="15dp"
android:text="News" />
<ImageButton
android:layout_width="20dp"
android:layout_height="20dp"
android:padding="2dp"
android:layout_marginStart="5dp"
android:layout_marginBottom="-5dp"
android:scaleType="fitCenter"
android:background="@color/transparent"
android:src="@drawable/ic_edit"/>
</LinearLayout>
<TextView
android:id="@+id/text_group_meta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
android:textSize="12dp"
android:text="42 creators" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<com.futo.platformplayer.views.SearchView
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="0dp"
android:paddingEnd="0dp">
<LinearLayout
android:id="@+id/container_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_light"
android:text="@string/enabled" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:textColor="@color/gray_ac"
android:fontFamily="@font/inter_extra_light"
android:text="@string/these_creators_in_group" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_creators_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:paddingTop="10dp"
android:paddingBottom="10dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/container_disabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_light"
android:text="@string/disabled" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:textColor="@color/gray_ac"
android:fontFamily="@font/inter_extra_light"
android:text="@string/these_creators_not_in_group" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_creators_disabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
<FrameLayout
android:id="@+id/overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</androidx.recyclerview.widget.RecyclerView>
<FrameLayout
android:id="@+id/overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,99 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="2dp"
android:layout_margin="2dp"
android:clickable="true"
android:id="@+id/root">
<ImageView
android:id="@+id/thumb"
android:layout_width="50dp"
android:layout_height="match_parent"
android:padding="12dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:src="@drawable/ic_dragdrop_white"
android:background="@color/transparent"
/>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="75dp"
android:layout_height="50dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/thumb"
android:layout_marginLeft="10dp"
android:scaleType="centerCrop"
android:src="@drawable/xp_book" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintLeft_toRightOf="@id/image"
app:layout_constraintRight_toLeftOf="@id/buttons"
android:layout_marginLeft="10dp"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/text_sub_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="12dp"
android:textColor="@color/white"
android:textAlignment="textStart"
android:text="News" />
<TextView
android:id="@+id/text_sub_group_meta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="10dp"
android:textColor="@color/gray_ac"
android:textAlignment="textStart"
android:text="News" />
</LinearLayout>
<LinearLayout
android:id="@+id/buttons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="horizontal">
<ImageButton
android:id="@+id/button_trash"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_marginRight="10dp"
android:padding="10dp"
android:scaleType="fitCenter"
android:background="@color/transparent"
app:tint="@color/pastel_red"
android:src="@drawable/ic_trash"
/>
<ImageButton
android:id="@+id/button_settings"
android:layout_width="50dp"
android:layout_height="match_parent"
android:padding="10dp"
android:scaleType="fitCenter"
android:background="@color/transparent"
android:src="@drawable/ic_settings"
android:visibility="gone"
/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,134 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
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="Select an Image"
app:metadata=""
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toTopOf="@id/container_select"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
<LinearLayout
android:id="@+id/gallery_selected_container"
android:layout_width="150dp"
android:layout_height="100dp"
android:gravity="center_horizontal"
android:padding="2dp"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp">
<ImageView
android:id="@+id/gallery_selected"
android:layout_width="150dp"
android:layout_height="100dp"
android:scaleType="centerCrop" />
</LinearLayout>
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_gallery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:buttonIcon="@drawable/ic_gallery"
app:buttonText="Open Photo Gallery"
app:buttonSubText="Pick an image from the gallery" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:textSize="20dp"
android:text="Preset" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textColor="@color/gray_ac"
android:textSize="12dp"
android:text="Pick a preset image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_presets"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:textSize="20dp"
android:text="Creator" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textColor="@color/gray_ac"
android:textSize="12dp"
android:text="Pick a creator as group image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_creators"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/container_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<Button
android:id="@+id/button_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="Select" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:orientation="vertical"
android:id="@+id/root">
<EditText
android:id="@+id/edit_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:imeOptions="actionDone"
android:singleLine="true"
android:hint="Search"
android:paddingEnd="46dp" />
<ImageButton
android:id="@+id/button_clear_search"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:layout_gravity="right|center_vertical"
android:visibility="invisible"
android:src="@drawable/ic_clear_16dp" />
</FrameLayout>

View file

@ -23,4 +23,9 @@
android:layout_height="wrap_content"
android:orientation="horizontal" />
</ScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_subgroups"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</LinearLayout>

View file

@ -0,0 +1,36 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="78dp"
android:layout_height="54dp"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="2dp"
android:layout_margin="2dp"
android:clickable="true"
android:id="@+id/root">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/xp_book" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#99000000"
android:gravity="center">
<TextView
android:id="@+id/text_sub_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="12dp"
android:textAlignment="center"
android:text="News" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:layout_height="32dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:background="@drawable/background_pill"

View file

@ -69,6 +69,8 @@
<string name="discover">Discover</string>
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
<string name="these_creators_in_group">These are the creators that are visible for this group.</string>
<string name="these_creators_not_in_group">These creators are not in this group.</string>
<string name="disabled">Disabled</string>
<string name="watch_later">Watch Later</string>
<string name="create">Create</string>
@ -346,6 +348,9 @@
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
<string name="background_switch_audio">Switch to Audio in Background</string>
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
<string name="subscription_group_menu">Groups</string>
<string name="show_subscription_group">Show Subscription Groups</string>
<string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string>
<string name="preview_feed_items">Preview Feed Items</string>
<string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string>
<string name="log_level">Log Level</string>
@ -564,6 +569,7 @@
<string name="playlist_copied_as_local_playlist">Playlist copied as local playlist</string>
<string name="are_you_sure_you_want_to_delete_the_downloaded_videos">Are you sure you want to delete the downloaded videos?</string>
<string name="create_new_playlist">Create new playlist</string>
<string name="create_new_subgroup">Create new subscription group</string>
<string name="expected_media_content_found">Expected media content, found</string>
<string name="failed_to_load_post">Failed to load post.</string>
<string name="replies">replies</string>