diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt index 39944ee5..71516d7f 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt @@ -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()); \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/PresetImages.kt b/app/src/main/java/com/futo/platformplayer/PresetImages.kt new file mode 100644 index 00000000..cbdf2834 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/PresetImages.kt @@ -0,0 +1,20 @@ +package com.futo.platformplayer + +class PresetImages { + companion object { + val images = mapOf( + 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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index e0f6e96b..6237f56c 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -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(); diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index f1a0ea44..bac10f4a 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -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(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(), 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); diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 294e4fcf..6dfe8472 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -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"); } } diff --git a/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt index d191f642..ed42b722 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/ManageTabsActivity.kt @@ -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 -> diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/AdhocRequestModifier.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/AdhocRequestModifier.kt new file mode 100644 index 00000000..ea250ae4 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/AdhocRequestModifier.kt @@ -0,0 +1,14 @@ +package com.futo.platformplayer.api.media.models.modifier + +class AdhocRequestModifier: IRequestModifier { + val _handler: (String, Map)->IRequest; + override var allowByteSkip: Boolean = false; + + constructor(modifyReq: (String, Map)->IRequest) { + _handler = modifyReq; + } + + override fun modifyRequest(url: String, headers: Map): IRequest { + return _handler(url, headers); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IModifierOptions.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IModifierOptions.kt new file mode 100644 index 00000000..1a7e4ce6 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IModifierOptions.kt @@ -0,0 +1,6 @@ +package com.futo.platformplayer.api.media.models.modifier + +interface IModifierOptions { + val applyAuthClient: String?; + val applyCookieClient: String?; +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequest.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequest.kt new file mode 100644 index 00000000..43cd502c --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequest.kt @@ -0,0 +1,6 @@ +package com.futo.platformplayer.api.media.models.modifier + +interface IRequest { + val url: String?; + val headers: Map; +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequestModifier.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequestModifier.kt new file mode 100644 index 00000000..f15ee477 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/modifier/IRequestModifier.kt @@ -0,0 +1,7 @@ +package com.futo.platformplayer.api.media.models.modifier + + +interface IRequestModifier { + var allowByteSkip: Boolean; + fun modifyRequest(url: String, headers: Map): IRequest +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt index 1b929293..47dfb1cb 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt @@ -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? = 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 { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index f5f72686..7106f02f 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -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 = 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>?): IPager = 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 = 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>?): IPager = 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(); } - 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 { 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 diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt index 5833295e..e6a49777 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt @@ -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? = 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) { + constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List, settings: HashMap? = null) { this.config = config; this.authEncrypted = authEncrypted; this.captchaEncrypted = captchaEncrypted; this.flags = flags; + this.settings = settings ?: hashMapOf(); } fun getSettingsWithDefaults(): HashMap { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt index 05df143f..8496dfc7 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt @@ -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>; + private var _otherCookieMap: HashMap>; 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, 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(); + 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(); - _currentCookieMap[domainToUse] = newMap - newMap; + if ((_auth != null || _currentCookieMap.isNotEmpty())) { + val cookieMap = if (_currentCookieMap.containsKey(domainToUse)) + _currentCookieMap[domainToUse]!!; + else { + val newMap = hashMapOf(); + _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(); + _otherCookieMap[domainToUse] = newMap + newMap; + } + if (cookieMap.containsKey(cookie.first) || doAllowNewCookies) + cookieMap[cookie.first] = cookieValue; } - if(cookieMap.containsKey(cookie.first) || doAllowNewCookies) - cookieMap[cookie.first] = cookieValue; } } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt index 09da4562..a6a15fb6 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt index fad34868..6ec2fd98 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt @@ -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}"); } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt index 01253896..683c64af 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChannelPager.kt @@ -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, IPager { - 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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt index 04146582..ab847b6b 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt @@ -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("getReplies", arrayOf()); - return JSCommentPager(_config!!, _plugin!!, obj); + val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient"); + return JSCommentPager(_config!!, plugin, obj); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSCommentPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSCommentPager.kt index 13d44fe9..94205d35 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSCommentPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSCommentPager.kt @@ -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, IPager { - 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); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt index f7a8fbbc..490fa7c4 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContentPager.kt @@ -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, 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); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt index 1d8ead85..27731fea 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt @@ -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, 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"); } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt index 831cc93d..a7672823 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt @@ -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 : IPager { - protected val plugin: V8Plugin; + protected val plugin: JSClient; protected val config: SourcePluginConfig; protected var pager: V8ValueObject; @@ -21,9 +22,9 @@ abstract class JSPager : IPager { 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 : IPager { 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()); }; _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt index 97b3f29b..cbd4e013 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistDetails.kt @@ -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; - 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")); } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistPager.kt index a0df057b..151bfe2a 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaylistPager.kt @@ -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, IPager { - 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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt index e8982889..bc455fcc 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt @@ -54,6 +54,6 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails { private fun getCommentsJS(client: JSClient): JSCommentPager { val commentPager = _content.invoke("getComments", arrayOf()); - return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager); + return JSCommentPager(_pluginConfig, client, commentPager); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequest.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequest.kt index 960982bf..1a9ef32b 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequest.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequest.kt @@ -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; +class JSRequest : IRequest { + private val _v8Url: String?; + private val _v8Headers: Map?; + private val _v8Options: Options?; - constructor(config: IV8PluginConfig, obj: V8ValueObject) { + override var url: String? = null; + override lateinit var headers: Map; + + constructor(plugin: JSClient, url: String?, headers: Map?, options: Options?, originalUrl: String?, originalHeaders: Map?) { + _v8Url = url; + _v8Headers = headers; + _v8Options = options; + initialize(plugin, originalUrl, originalHeaders); + } + constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map?) { val contextName = "ModifyRequestResponse"; - url = obj.getOrThrow(config, "url", contextName); - headers = obj.getOrThrow(config, "headers", contextName); + val config = plugin.config; + _v8Url = obj.getOrDefault(config, "url", contextName, null); + _v8Headers = obj.getOrDefault>(config, "headers", contextName, null); + _v8Options = obj.getOrDefault(config, "options", "JSRequestModifier.options", null)?.let { + Options(config, it); + } + initialize(plugin, originalUrl, originalHeaders); + } + + private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map?) { + 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?): 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 { + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt index 0d71057a..cffbb474 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt @@ -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): IRequest { + override fun modifyRequest(url: String, headers: Map): IRequest { if (_modifier.isClosed) { return Request(url, headers); } val result = V8Plugin.catchScriptErrors(_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; - } data class Request(override val url: String, override val headers: Map) : IRequest; } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt index 815452ee..83137f23 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt @@ -44,13 +44,14 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { override val subtitles: List; - 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(config, "dash", contextName)); - hls = JSSource.fromV8HLSNullable(config, _content.getOrThrowNullable(config, "hls", contextName)); - live = JSSource.fromV8VideoNullable(config, _content.getOrThrowNullable(config, "live", contextName)); + video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); + dash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable(config, "dash", contextName)); + hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable(config, "hls", contextName)); + live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable(config, "live", contextName)); rating = IRating.fromV8OrDefault(config, _content.getOrDefault(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); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoPager.kt index 1cc24a2d..89117138 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoPager.kt @@ -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 { - 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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt index 39d80032..09de1f35 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt index 9eafafee..42eeffce 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioWithMetadataSource.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt index 4f50cbf6..3070a2d4 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt index 8636cbb8..41948802 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt @@ -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); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt index 8c785c17..606d107c 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt index 9bf35ad2..ccd9c223 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt @@ -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(_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}"); } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSUnMuxVideoSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSUnMuxVideoSourceDescriptor.kt index 08d4911d..035f5fb6 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSUnMuxVideoSourceDescriptor.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSUnMuxVideoSourceDescriptor.kt @@ -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; override val audioSources: Array; - 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(config, "videoSources", contextName).toArray() - .map { JSSource.fromV8Video(config, it as V8ValueObject) } + .map { JSSource.fromV8Video(plugin, it as V8ValueObject) } .toTypedArray(); this.audioSources = obj.getOrThrow(config, "audioSources", contextName).toArray() - .map { JSSource.fromV8Audio(config, it as V8ValueObject) } + .map { JSSource.fromV8Audio(plugin, it as V8ValueObject) } .toTypedArray(); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt index 463100b0..131a5794 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt @@ -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; - 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(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}"); } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt index a4fbf0e8..66246f56 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt index 90b6edee..0e86c2fc 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoWithMetadataSource.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index fe1dad58..9dadb54e 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -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 diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt index 95ad1b3a..8b7c08da 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -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 = ConcurrentHashMap(); val httpClient: ManagedHttpClient get() = _client; val httpClientAuth: ManagedHttpClient get() = _clientAuth; + val httpClientOthers: Map get() = _clientOthers; + + fun registerHttpClient(client: JSHttpClient) { + synchronized(_clientOthers) { + _clientOthers.put(client.clientId, client); + } + } private val _runtimeLock = Object(); var _runtime : V8Runtime? = null; diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt index 68a74202..937e2d23 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt @@ -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(); + @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 diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index 48f68590..979a679a 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -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() }), ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate() }), ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate() }), + ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate() }), 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()"); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt index a28e8016..3af3f01d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt @@ -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) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt new file mode 100644 index 00000000..f77df2cd --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupFragment.kt @@ -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 = arrayListOf(); + private val _disabledCreators: ArrayList = arrayListOf(); + private val _enabledCreatorsFiltered: ArrayList = arrayListOf(); + private val _disabledCreatorsFiltered: ArrayList = arrayListOf(); + + private val _containerEnabled: LinearLayout; + private val _containerDisabled: LinearLayout; + + private val _recyclerCreatorsEnabled: AnyAdapterView; + private val _recyclerCreatorsDisabled: AnyAdapterView; + + 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(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) { + it.itemView.setPadding(0, dp6, 0, dp6); + it.onClick.subscribe { channel -> + disableCreator(channel); + }; + } + _recyclerCreatorsDisabled = findViewById(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"; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt new file mode 100644 index 00000000..52ed90dd --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionGroupListFragment.kt @@ -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 = arrayListOf(); + private var _list: AnyAdapterView? = 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(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(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 {} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index cd5dca38..41578d00 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -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 { override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar + private var _subGroup: SubscriptionGroup? = null; + constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = 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(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(g); + }; synchronized(_filterLock) { _subscriptionBar?.setToggles( @@ -288,9 +303,15 @@ class SubscriptionsFeedFragment : MainFragment() { override fun filterResults(results: List): List { 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; diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index 1625b1e4..9ca4aa8e 100644 --- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt +++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt @@ -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 { diff --git a/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt b/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt index 1497b52d..1de1f917 100644 --- a/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt +++ b/app/src/main/java/com/futo/platformplayer/models/ImageVariable.kt @@ -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)); } diff --git a/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt b/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt new file mode 100644 index 00000000..c46aa3b8 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/models/SubscriptionGroup.kt @@ -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 = 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("+"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index 45e8e069..bcf68592 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -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(); + } } } diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt b/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt index 8d9ceff6..7b6628e6 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDeveloper.kt @@ -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) { + val client = StatePlatform.instance.getDevClient(); + client?.let { + it.descriptor.settings = settings; + }; + } + fun runServer() { if(_server != null) diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt index 85b16fd0..2f08937d 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt @@ -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) { 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) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt new file mode 100644 index 00000000..f77e2fad --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt @@ -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("subscription_groups") + .withUnique { it.id } + .load(); + + val onGroupsChanged = Event0(); + + fun getSubscriptionGroup(id: String): SubscriptionGroup? { + return _subGroups.findItem { it.id == id }; + } + fun getSubscriptionGroups(): List { + 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; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt b/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt index 2ff1e01f..db295aa3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/AnyAdapterView.kt @@ -46,9 +46,10 @@ class AnyAdapterView(view: RecyclerView, adapter: BaseAnyAdapter, where T : AnyAdapter.AnyViewHolder{ companion object { + /* inline fun > RecyclerView.asAny(list: List, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView { return asAny(ArrayList(list), orientation, reversed, onCreate); - } + }*/ inline fun > RecyclerView.asAny(list: ArrayList, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView { return AnyAdapterView(this, AnyAdapter.create(list, onCreate), orientation, reversed); } diff --git a/app/src/main/java/com/futo/platformplayer/views/SearchView.kt b/app/src/main/java/com/futo/platformplayer/views/SearchView.kt new file mode 100644 index 00000000..77cf431e --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/SearchView.kt @@ -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(); + + 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()); + }; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt index 225c498b..d5ca59f0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/AnyAdapter.kt @@ -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, IT : ViewHolder> { cb(item); } + @SuppressLint("NotifyDataSetChanged") fun notifyContentChanged() { adapter.notifyDataSetChanged(); } @@ -116,7 +121,6 @@ class AnyAdapter> : BaseAnyAdapter { private class Adapter> : RecyclerView.Adapter { private val _parent: AnyAdapter; - constructor(parentAdapter: AnyAdapter) { _parent = parentAdapter; } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt new file mode 100644 index 00000000..b1338753 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt @@ -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( + 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(); + + private val _taskLoadProfile = TaskHandler( + StateApp.instance.scopeGetter, + { PolycentricCache.instance.getProfileAsync(it) }) + .success { onProfileLoaded(it, true) } + .exception { + 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(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( + 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(); + + private val _taskLoadProfile = TaskHandler( + StateApp.instance.scopeGetter, + { PolycentricCache.instance.getProfileAsync(it) }) + .success { onProfileLoaded(it, true) } + .exception { + 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(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) +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt new file mode 100644 index 00000000..0f93a5eb --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupBarViewHolder.kt @@ -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( + 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(); + val onClickLong = Event1(); + + 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"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt new file mode 100644 index 00000000..19fe8a30 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionGroupListViewHolder.kt @@ -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( + 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(); + val onSettings = Event1(); + val onDelete = Event1(); + val onDragDrop = Event1(); + + 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"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt new file mode 100644 index 00000000..0971d577 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/ImageVariableOverlay.kt @@ -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; + private val _recyclerCreators: AnyAdapterView; + + private val _creators: ArrayList = arrayListOf(); + private val _presets: ArrayList = + ArrayList(PresetImages.images.map { PresetImage(it.value, it.key, false) }); + + private var _selected: ImageVariable? = null; + private var _selectedFile: String? = null; + + val onSelected = Event1(); + val onClose = Event0(); + + constructor(context: Context, creatorFilters: List? = 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(R.id.recycler_presets).asAny(_presets, RecyclerView.HORIZONTAL) { + it.onClick.subscribe { + _selected = ImageVariable.fromPresetName(it.name); + updateSelected(); + }; + }; + val dp6 = 6.dp(resources); + _recyclerCreators = findViewById(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(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(); + 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); +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuTextInput.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuTextInput.kt index ca4a39a9..9a53fe44 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuTextInput.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuTextInput.kt @@ -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(); diff --git a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt index d001cdc5..a456cba0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt +++ b/app/src/main/java/com/futo/platformplayer/views/subscriptions/SubscriptionBar.kt @@ -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? = null; + private var _subGroups: AnyAdapterView private val _tagsContainer: LinearLayout; + private val _groups: ArrayList; + private var _group: SubscriptionGroup? = null; + val onClickChannel = Event1(); + val onToggleGroup = Event1(); + val onHoldGroup = Event1(); + 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(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) { it.onClick.subscribe { c -> onClickChannel.emit(c.channel); }; }; + _groups = ArrayList(getGroups()); + _subGroups = findViewById(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 { + 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(); diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index abe86c62..25100c2f 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -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)); } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java index d153c440..4c99ccb9 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java +++ b/app/src/main/java/com/futo/platformplayer/views/video/datasources/JSHttpDataSource.java @@ -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 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(); } diff --git a/app/src/main/res/drawable/background_primary_rounded_2dp.xml b/app/src/main/res/drawable/background_primary_rounded_2dp.xml new file mode 100644 index 00000000..1eafcfd9 --- /dev/null +++ b/app/src/main/res/drawable/background_primary_rounded_2dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_gallery.xml b/app/src/main/res/drawable/ic_gallery.xml new file mode 100644 index 00000000..27d3ffc6 --- /dev/null +++ b/app/src/main/res/drawable/ic_gallery.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/xp_book.jpg b/app/src/main/res/drawable/xp_book.jpg new file mode 100644 index 00000000..6ff22bb6 Binary files /dev/null and b/app/src/main/res/drawable/xp_book.jpg differ diff --git a/app/src/main/res/drawable/xp_code.jpg b/app/src/main/res/drawable/xp_code.jpg new file mode 100644 index 00000000..00dfb956 Binary files /dev/null and b/app/src/main/res/drawable/xp_code.jpg differ diff --git a/app/src/main/res/drawable/xp_controller.jpg b/app/src/main/res/drawable/xp_controller.jpg new file mode 100644 index 00000000..bd6aeaa0 Binary files /dev/null and b/app/src/main/res/drawable/xp_controller.jpg differ diff --git a/app/src/main/res/drawable/xp_forest.jpg b/app/src/main/res/drawable/xp_forest.jpg new file mode 100644 index 00000000..e0428a2f Binary files /dev/null and b/app/src/main/res/drawable/xp_forest.jpg differ diff --git a/app/src/main/res/drawable/xp_laptop.jpg b/app/src/main/res/drawable/xp_laptop.jpg new file mode 100644 index 00000000..d33568c0 Binary files /dev/null and b/app/src/main/res/drawable/xp_laptop.jpg differ diff --git a/app/src/main/res/layout/fragment_subscriptions_group.xml b/app/src/main/res/layout/fragment_subscriptions_group.xml new file mode 100644 index 00000000..cf728852 --- /dev/null +++ b/app/src/main/res/layout/fragment_subscriptions_group.xml @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_subscriptions_group_list.xml b/app/src/main/res/layout/fragment_subscriptions_group_list.xml new file mode 100644 index 00000000..0c0b91b1 --- /dev/null +++ b/app/src/main/res/layout/fragment_subscriptions_group_list.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_subscription_group.xml b/app/src/main/res/layout/list_subscription_group.xml new file mode 100644 index 00000000..d7c1d7bc --- /dev/null +++ b/app/src/main/res/layout/list_subscription_group.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/overlay_image_variable.xml b/app/src/main/res/layout/overlay_image_variable.xml new file mode 100644 index 00000000..b89d7fd4 --- /dev/null +++ b/app/src/main/res/layout/overlay_image_variable.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +