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 39944ee57b8a7c6bed635bfa0c6fda24e74469d1..71516d7f916a5399a501f279dc2028680c6c0ce1 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 0000000000000000000000000000000000000000..cbdf2834f63f3b8e52194f63d9cc4267bbab4b2b --- /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<String, Int>( + Pair("xp_book", R.drawable.xp_book), + Pair("xp_forest", R.drawable.xp_forest), + Pair("xp_code", R.drawable.xp_code), + Pair("xp_controller", R.drawable.xp_controller), + Pair("xp_laptop", R.drawable.xp_laptop) + ); + + fun getPresetResIdByName(name: String): Int { + return images[name] ?: -1; + } + fun getPresetNameByResId(id: Int): String? { + return images.entries.find { it.value == id }?.key; + } + } +} \ 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 e0f6e96bab4cd325e7b7dbdf2c3cb839f6a1820d..6237f56c7f6c1d6065bc10e5e70566f8c2c38022 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 f1a0ea440bcf5fbfd0c952a9d1c305f38c950af8..bac10f4a868c982dd6e2ec231dbb424e888738ea 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<View>(LoaderView(container.context)) val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items) @@ -512,6 +532,48 @@ class UISlideOverlays { return overlay; } + fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel? = null, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay { + val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name)); + val addSubGroupOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_subgroup), container.context.getString(R.string.ok), false, nameInput); + + addSubGroupOverlay.onOK.subscribe { + val text = nameInput.text; + if (text.isBlank()) { + return@subscribe; + } + + addSubGroupOverlay.hide(); + nameInput.deactivate(); + nameInput.clear(); + if(onCreate == null) + { + //TODO: Do this better, temp + StateApp.instance.contextOrNull?.let { + if(it is MainActivity) { + val subGroup = SubscriptionGroup(text); + if(initialChannel != null) { + subGroup.urls.add(initialChannel.url); + if(initialChannel.thumbnail != null) + subGroup.image = ImageVariable(initialChannel.thumbnail); + } + it.navigate(it.getFragment<SubscriptionGroupFragment>(), subGroup); + } + } + } + else + onCreate(text) + }; + + addSubGroupOverlay.onCancel.subscribe { + nameInput.deactivate(); + nameInput.clear(); + }; + + addSubGroupOverlay.show(); + nameInput.activate(); + + return addSubGroupOverlay + } fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay { val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name)); val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput); 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 294e4fcf22c8b3b998b0b039044e192d5ff295b7..6dfe8472a73e9e79fa3db669140457eef2667c59 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 d191f6421922722a0d6f38dcd2b726ea84276a99..ed42b7229f5f103f2ee29a917d03845d5c803527 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 0000000000000000000000000000000000000000..ea250ae450db058a52164ac4c77788356fcf028a --- /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<String,String>)->IRequest; + override var allowByteSkip: Boolean = false; + + constructor(modifyReq: (String, Map<String,String>)->IRequest) { + _handler = modifyReq; + } + + override fun modifyRequest(url: String, headers: Map<String, String>): IRequest { + return _handler(url, headers); + } +} \ 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 0000000000000000000000000000000000000000..1a7e4ce6b0933395be00b23224d6ae779f8608e4 --- /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 0000000000000000000000000000000000000000..43cd502c1ba495e247e77bcd1e661117329d991c --- /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<String, String>; +} \ 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 0000000000000000000000000000000000000000..f15ee4771eba3fe9f6bd517d86b8a966a1acad31 --- /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<String, String>): 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 1b929293260632f6fa6fdbd45336209b354f7129..47dfb1cb6c716f9de28b526186af88fd4adf1d7b 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<String, String?>? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV"), settings), null, script) { _devScript = script; _auth = auth; _captcha = captcha; @@ -49,7 +49,7 @@ class DevJSClient : JSClient { _auth = auth; } fun recreate(context: Context): DevJSClient { - return DevJSClient(context, config, _devScript, _auth, _captcha, devID); + return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings); } override fun getCopy(): JSClient { 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 f5f726864f689cc6d7aa4d096e5ccf2565b561f4..7106f02fbe78671dfc9eabf97861e013e324c8bc 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<IPlatformContent> = isBusyWith { ensureEnabled(); - return@isBusyWith JSContentPager(config, plugin, + return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.getHome()")); } @@ -292,7 +300,7 @@ open class JSClient : IPlatformClient { @JSDocsParameter("channelId", "(optional) Channel id to search in") override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { ensureEnabled(); - return@isBusyWith JSContentPager(config, plugin, + return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); } @@ -316,7 +324,7 @@ open class JSClient : IPlatformClient { if(!capabilities.hasSearchChannelContents) throw IllegalStateException("This plugin does not support channel search"); - return@isBusyWith JSContentPager(config, plugin, + return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); } @@ -325,7 +333,7 @@ open class JSClient : IPlatformClient { @JSDocsParameter("query", "Query that channels should match") override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith { ensureEnabled(); - return@isBusyWith JSChannelPager(config, plugin, + return@isBusyWith JSChannelPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})")); } @@ -372,7 +380,7 @@ open class JSClient : IPlatformClient { @JSDocsParameter("filters", "(optional) Filters to apply on contents") override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith { ensureEnabled(); - return@isBusyWith JSContentPager(config, plugin, + return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); } @@ -438,7 +446,7 @@ open class JSClient : IPlatformClient { @JSDocsParameter("url", "A content url (this platform)") override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith { ensureEnabled(); - return@isBusyWith IJSContentDetails.fromV8(config, + return@isBusyWith IJSContentDetails.fromV8(this, plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})")); } @@ -476,13 +484,13 @@ open class JSClient : IPlatformClient { if (pager !is V8ValueObject) { //TODO: Maybe solve this better return@isBusyWith EmptyPager<IPlatformComment>(); } - return@isBusyWith JSCommentPager(config, plugin, pager); + return@isBusyWith JSCommentPager(config, this, pager); } @JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment") @JSDocsParameter("comment", "Comment object that was returned by getComments") override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> { ensureEnabled(); - return comment.getReplies(this) ?: JSCommentPager(config, plugin, + return comment.getReplies(this) ?: JSCommentPager(config, this, plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})")); } @@ -501,7 +509,7 @@ open class JSClient : IPlatformClient { if(!capabilities.hasGetLiveEvents) return@isBusyWith null; ensureEnabled(); - return@isBusyWith JSLiveEventPager(config, plugin, + return@isBusyWith JSLiveEventPager(config, this, plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})")); } @JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform") @@ -514,7 +522,7 @@ open class JSClient : IPlatformClient { ensureEnabled(); if(!capabilities.hasSearchPlaylists) throw IllegalStateException("This plugin does not support playlist search"); - return@isBusyWith JSContentPager(config, plugin, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); + return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})")); } @JSOptional @JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform") @@ -530,7 +538,7 @@ open class JSClient : IPlatformClient { @JSDocsParameter("url", "Url of playlist") override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith { ensureEnabled(); - return@isBusyWith JSPlaylistDetails(plugin, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})")); + return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})")); } @JSOptional 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 5833295e9df2e403e92d843796788acf37aaefce..e6a49777762d368f358d500a6d14fe17698b0c59 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<String, String?>? = null) { this.config = config; this.authEncrypted = authEncrypted; this.captchaEncrypted = captchaEncrypted; this.flags = listOf(); + this.settings = settings ?: hashMapOf(); } - constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) { + constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>, settings: HashMap<String, String?>? = null) { this.config = config; this.authEncrypted = authEncrypted; this.captchaEncrypted = captchaEncrypted; this.flags = flags; + this.settings = settings ?: hashMapOf(); } fun getSettingsWithDefaults(): HashMap<String, String?> { 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 05df143fcdcb50ab66af662c01093be9fe4ea467..8496dfc788b40a331c0eb7291d201c0c059585da 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<String, HashMap<String, String>>; + private var _otherCookieMap: HashMap<String, HashMap<String, String>>; constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() { _jsClient = jsClient; @@ -28,6 +35,7 @@ class JSHttpClient : ManagedHttpClient { _captcha = captcha; _currentCookieMap = hashMapOf(); + _otherCookieMap = hashMapOf(); if(!auth?.cookieMap.isNullOrEmpty()) { for(domainCookies in auth!!.cookieMap!!) _currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value)); @@ -49,6 +57,45 @@ class JSHttpClient : ManagedHttpClient { return newClient; } + //TODO: Use this in beforeRequest to remove dup code + fun applyHeaders(url: Uri, headers: MutableMap<String, String>, applyAuth: Boolean = false, applyOtherCookies: Boolean = false) { + val domain = url.host!!.lowercase(); + val auth = _auth; + if (applyAuth && auth != null) { + //TODO: Possibly add doApplyHeaders + for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries }) + headers.put(header.key, header.value); + } + + if(doApplyCookies && (applyAuth || applyOtherCookies)) { + val cookiesToApply = hashMapOf<String, String>(); + if(applyOtherCookies) + synchronized(_otherCookieMap) { + for(cookie in _otherCookieMap + .filter { domain.matchesDomain(it.key) } + .flatMap { it.value.toList() }) + cookiesToApply[cookie.first] = cookie.second; + } + if(applyAuth) + synchronized(_currentCookieMap) { + for(cookie in _currentCookieMap + .filter { domain.matchesDomain(it.key) } + .flatMap { it.value.toList() }) + cookiesToApply[cookie.first] = cookie.second; + }; + + if(cookiesToApply.size > 0) { + val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; "); + + val existingCookies = headers["Cookie"]; + if(!existingCookies.isNullOrEmpty()) + headers.put("Cookie", existingCookies.trim(';') + "; " + cookieString); + else + headers.put("Cookie", cookieString); + } + } + } + override fun beforeRequest(request: okhttp3.Request): okhttp3.Request { val domain = request.url.host.lowercase(); val auth = _auth; @@ -101,10 +148,10 @@ class JSHttpClient : ManagedHttpClient { val defaultCookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString("."); for (header in resp.headers) { - if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") { + if(header.first.lowercase() == "set-cookie") { + var domainToUse = domain; val cookie = cookieStringToPair(header.second); var cookieValue = cookie.second; - var domainToUse = domain; if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) { val cookieParts = cookie.second.split(";"); @@ -124,17 +171,33 @@ class JSHttpClient : ManagedHttpClient { domainToUse = if (cookieVariables.containsKey("domain")) cookieVariables["domain"]!!.lowercase(); else defaultCookieDomain; + //TODO: Make sure this has no negative effect besides apply cookies to root domain + if(!domainToUse.startsWith(".")) + domainToUse = ".${domainToUse}"; } - val cookieMap = if (_currentCookieMap.containsKey(domainToUse)) - _currentCookieMap[domainToUse]!!; + if ((_auth != null || _currentCookieMap.isNotEmpty())) { + val cookieMap = if (_currentCookieMap.containsKey(domainToUse)) + _currentCookieMap[domainToUse]!!; + else { + val newMap = hashMapOf<String, String>(); + _currentCookieMap[domainToUse] = newMap + newMap; + } + if (cookieMap.containsKey(cookie.first) || doAllowNewCookies) + cookieMap[cookie.first] = cookieValue; + } else { - val newMap = hashMapOf<String, String>(); - _currentCookieMap[domainToUse] = newMap - newMap; + val cookieMap = if (_otherCookieMap.containsKey(domainToUse)) + _otherCookieMap[domainToUse]!!; + else { + val newMap = hashMapOf<String, String>(); + _otherCookieMap[domainToUse] = newMap + newMap; + } + if (cookieMap.containsKey(cookie.first) || doAllowNewCookies) + cookieMap[cookie.first] = cookieValue; } - if(cookieMap.containsKey(cookie.first) || doAllowNewCookies) - cookieMap[cookie.first] = cookieValue; } } } 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 09da4562761b5c9d6abe5352ac28c117954335c1..a6a15fb63817667c2a78496e6b8c13f38100d3ba 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 fad34868de7d3aae94889f5604acea08c7f2fbae..6ec2fd98204e92c63e39c6734bb76bbb45009993 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 012538961e469b1a92f84a3ac727eac3b4813583..683c64afde8371f06e2d0a5a33fde148fa506c6e 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<PlatformAuthorLink>, IPager<PlatformAuthorLink> { - constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {} + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {} override fun convertResult(obj: V8ValueObject): PlatformAuthorLink { return PlatformAuthorLink.fromV8(config, obj); 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 04146582388801e1cef8650491289638b09c04a8..ab847b6bf229c24cfc1387c4fa839aa56c8687fc 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<V8ValueObject>("getReplies", arrayOf<Any>()); - return JSCommentPager(_config!!, _plugin!!, obj); + val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient"); + return JSCommentPager(_config!!, plugin, obj); } } \ 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 13d44fe9e2ead5f3c13a00e84c61fe1ddf21c933..94205d35168a70d24eac48254090c794e4b24af7 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<IPlatformComment>, IPager<IPlatformComment> { - constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) { } + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) { } override fun convertResult(obj: V8ValueObject): IPlatformComment { - return JSComment(config, plugin, obj); + return JSComment(config, plugin.getUnderlyingPlugin(), obj); } } \ 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 f7a8fbbc304270978846a72309074a6823a9f50d..490fa7c4df55160aab2f4babea25132c718cde40 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<IPlatformContent>, IPluginSourced { override val sourceConfig: SourcePluginConfig get() = config; - constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {} + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {} override fun convertResult(obj: V8ValueObject): IPlatformContent { - return IJSContent.fromV8(config, obj); + return IJSContent.fromV8(plugin, obj); } } \ 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 1d8ead850f3f126d3780b313aab4a2d58849042c..27731feac4435356910d055b1d3f7877f1b51059 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<IPlatformLiveEvent>, IPlatformLiveEventPager { override var nextRequest: Int; - constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) { + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) { nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); } 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 831cc93d0120400cce59a2a35f44460205bc8329..a767282310850854d172a6042679335bd9fc9ac0 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<T> : IPager<T> { - protected val plugin: V8Plugin; + protected val plugin: JSClient; protected val config: SourcePluginConfig; protected var pager: V8ValueObject; @@ -21,9 +22,9 @@ abstract class JSPager<T> : IPager<T> { private var _hasMorePages: Boolean = false; //private var _morePagesWasFalse: Boolean = false; - val isAvailable get() = plugin._runtime?.let { !it.isClosed && !it.isDead } ?: false; + val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false; - constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) { + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) { this.plugin = plugin; this.pager = pager; this.config = config; @@ -43,7 +44,7 @@ abstract class JSPager<T> : IPager<T> { override fun nextPage() { warnIfMainThread("JSPager.nextPage"); - pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { + pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { pager.invoke("nextPage", arrayOf<Any>()); }; _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; 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 97b3f29b0fbc32ae4292434e61dd96339c3d78c2..cbd4e0138c7499023d3099a824cc26f4077776c4 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<IPlatformVideo>; - constructor(plugin: V8Plugin, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) { + constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) { contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails")); } 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 a0df057b0a5fa2a4f129c273f3e3d4a4da9b2aae..151bfe2afd8a8840d102a56f340a77e7c2ea66f7 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<IPlatformPlaylist>, IPager<IPlatformPlaylist> { - constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {} + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {} override fun convertResult(obj: V8ValueObject): IPlatformPlaylist { return JSPlaylist(config, obj); 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 e898288946fbaeeff2da8476be0e77ce898ad284..bc455fcc1d1a33c13fe5625af33c4299c91bb54b 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<V8ValueObject>("getComments", arrayOf<Any>()); - 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 960982bf1018a07fd826d55efd2ffd9cea128bf4..1a9ef32b4fd0218e8bde298ff41b13b9ab0542f5 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<String, String>; +class JSRequest : IRequest { + private val _v8Url: String?; + private val _v8Headers: Map<String, String>?; + private val _v8Options: Options?; - constructor(config: IV8PluginConfig, obj: V8ValueObject) { + override var url: String? = null; + override lateinit var headers: Map<String, String>; + + constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) { + _v8Url = url; + _v8Headers = headers; + _v8Options = options; + initialize(plugin, originalUrl, originalHeaders); + } + constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?) { val contextName = "ModifyRequestResponse"; - url = obj.getOrThrow(config, "url", contextName); - headers = obj.getOrThrow(config, "headers", contextName); + val config = plugin.config; + _v8Url = obj.getOrDefault<String>(config, "url", contextName, null); + _v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null); + _v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let { + Options(config, it); + } + initialize(plugin, originalUrl, originalHeaders); + } + + private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) { + val config = plugin.config; + url = _v8Url ?: originalUrl; + headers = _v8Headers ?: originalHeaders ?: mapOf(); + + if(_v8Options != null) { + if(_v8Options.applyCookieClient != null && url != null) { + val client = plugin.getHttpClientById(_v8Options.applyCookieClient); + if(client != null) { + val toModifyHeaders = headers.toMutableMap(); + client.applyHeaders(Uri.parse(url), toModifyHeaders, false, true); + headers = toModifyHeaders; + } + } + if(_v8Options.applyAuthClient != null && url != null) { + val client = plugin.getHttpClientById(_v8Options.applyAuthClient); + if(client != null) { + val toModifyHeaders = headers.toMutableMap(); + client.applyHeaders(Uri.parse(url), toModifyHeaders, true, false); + headers = toModifyHeaders; + } + } + } + } + + fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest { + return JSRequest(plugin, _v8Url, _v8Headers, _v8Options, originalUrl, originalHeaders); + } + + + @kotlinx.serialization.Serializable + class Options: IModifierOptions { + override val applyAuthClient: String?; + override val applyCookieClient: String?; + + constructor(config: IV8PluginConfig, obj: V8ValueObject) { + applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null); + applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null); + } + } + + companion object { + } } \ 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 0d71057a093c6faee7252d89d45cf302113913cf..cffbb4743aea52a9e288430113e2fc3bc26b0c87 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<String, String>): IRequest { + override fun modifyRequest(url: String, headers: Map<String, String>): IRequest { if (_modifier.isClosed) { return Request(url, headers); } val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { _modifier.invoke("modifyRequest", url, headers); - }; + } as V8ValueObject; - return JSRequest(_config, result as V8ValueObject); + val req = JSRequest(_plugin, result, url, headers); + return req; } - interface IRequest { - val url: String; - val headers: Map<String, String>; - } data class Request(override val url: String, override val headers: Map<String, String>) : IRequest; } \ 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 815452ee6b5431da53d7e5ff2d9e9e16a7bde540..83137f23ecd06cf5493ea58e4e2e426e8a9710a9 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<ISubtitleSource>; - constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) { + constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { val contextName = "VideoDetails"; + val config = plugin.config; description = _content.getOrThrow(config, "description", contextName); - video = JSVideoSourceDescriptor.fromV8(config, _content.getOrThrow(config, "video", contextName)); - dash = JSSource.fromV8DashNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName)); - hls = JSSource.fromV8HLSNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName)); - live = JSSource.fromV8VideoNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName)); + video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); + dash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName)); + hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName)); + live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName)); rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0)); if(!_content.has("subtitles")) @@ -105,6 +106,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? return null; - return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager); + return JSCommentPager(_pluginConfig, client, commentPager); } } \ 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 1cc24a2d2dbf802e80970895f43f11ddd099dd14..89117138cd91ccb3a0e23af4a4cd1bb8ebaf6d03 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<IPlatformVideo> { - constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {} + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {} override fun convertResult(obj: V8ValueObject): IPlatformVideo { return JSVideo(config, obj); 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 39d800323f6a1e5d8875655b00693be00b0772cf..09de1f35674d04fa7eb457a1f28bf191b87b70ce 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 9eafafeeed83ec729e272f23a339a7cbf4cbf962..42eeffce95b3472707ccda96875d09fa76747fd2 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 4f50cbf685c5c86cf5ab3e92a41dd1ad0a3777a7..3070a2d4e26ed1df1a006f9b531225497352abc3 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 8636cbb810706884d95017ae905bdec86bc25d3b..4194880220a3b11c2c903b6328ed8e16172b85d5 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 8c785c1756ba3f51dcb9f96779625ba8b8164ab3..606d107c3811211528ad083c4dad91a99f165025 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 9bf35ad204819d5056caa4a33860de75b51f8f55..ccd9c22386b14248fca3ed359daa4d9ff7f7b884 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<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let { + JSRequest(plugin, it, null, null); + } + hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier"); } - fun getRequestModifier(): JSRequestModifier? { - if (!_hasRequestModifier || _obj.isClosed) { + fun getRequestModifier(): IRequestModifier? { + if(_requestModifier != null) + return AdhocRequestModifier { url, headers -> + return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers); + }; + + if (!hasRequestModifier || _obj.isClosed) { return null; } @@ -40,16 +56,7 @@ abstract class JSSource { return null; } - return JSRequestModifier(_config, result) - } - - fun getHttpDataSourceFactory(): HttpDataSource.Factory { - val requestModifier = getRequestModifier(); - return if (requestModifier != null) { - JSHttpDataSource.Factory().setRequestModifier(requestModifier); - } else { - DefaultHttpDataSource.Factory(); - } + return JSRequestModifier(_plugin, result) } companion object { @@ -60,28 +67,28 @@ abstract class JSSource { const val TYPE_DASH = "DashSource"; const val TYPE_HLS = "HLSSource"; - fun fromV8VideoNullable(config: IV8PluginConfig, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(config, it as V8ValueObject) }; - fun fromV8Video(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSource { + fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) }; + fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource { val type = obj.getString("plugin_type"); return when(type) { - TYPE_VIDEOURL -> JSVideoUrlSource(config, obj); - TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(config, obj); - TYPE_HLS -> fromV8HLS(config, obj); - TYPE_DASH -> fromV8Dash(config, obj); + TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj); + TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj); + TYPE_HLS -> fromV8HLS(plugin, obj); + TYPE_DASH -> fromV8Dash(plugin, obj); else -> throw NotImplementedError("Unknown type ${type}"); } } - fun fromV8DashNullable(config: IV8PluginConfig, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(config, it as V8ValueObject) }; - fun fromV8Dash(config: IV8PluginConfig, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(config, obj); - fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) }; - fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(config, obj); + fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) }; + fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj); + fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }; + fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj); - fun fromV8Audio(config: IV8PluginConfig, obj: V8ValueObject) : IAudioSource { + fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource { val type = obj.getString("plugin_type"); return when(type) { - TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(config, obj); - TYPE_AUDIOURL -> JSAudioUrlSource(config, obj); - TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(config, obj); + TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj); + TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj); + TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj); else -> throw NotImplementedError("Unknown type ${type}"); } } 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 08d4911d7ec395e33b2cfde2c9a50740a2c1857d..035f5fb64d13699aaf25335088981a3bf36964f0 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<IVideoSource>; override val audioSources: Array<IAudioSource>; - constructor(config: IV8PluginConfig, obj: V8ValueObject) { + constructor(plugin: JSClient, obj: V8ValueObject) { this._obj = obj; + val config = plugin.config; val contextName = "UnMuxVideoSource" this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName); this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray() - .map { JSSource.fromV8Video(config, it as V8ValueObject) } + .map { JSSource.fromV8Video(plugin, it as V8ValueObject) } .toTypedArray(); this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray() - .map { JSSource.fromV8Audio(config, it as V8ValueObject) } + .map { JSSource.fromV8Audio(plugin, it as V8ValueObject) } .toTypedArray(); } } \ 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 463100b05f02213fd53245e4be4aafc6ef5787a6..131a5794bf81578f58ba04981ae040ffb931f21f 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<IVideoSource>; - constructor(config: IV8PluginConfig, obj: V8ValueObject) { + constructor(plugin: JSClient, obj: V8ValueObject) { this._obj = obj; + val config = plugin.config; val contextName = "VideoSourceDescriptor"; this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName); this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray() - .map { JSSource.fromV8Video(config, it as V8ValueObject) } + .map { JSSource.fromV8Video(plugin, it as V8ValueObject) } .toTypedArray(); } @@ -28,11 +31,11 @@ class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor { const val TYPE_UNMUXED = "UnMuxVideoSourceDescriptor"; - fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSourceDescriptor { + fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor { val type = obj.getString("plugin_type") return when(type) { - TYPE_MUXED -> JSVideoSourceDescriptor(config, obj); - TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(config, obj); + TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj); + TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(plugin, obj); else -> throw NotImplementedError("Unknown type: ${type}"); } } 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 a4fbf0e80e02f53866d9e22f26329320aa640bad..66246f56fec6db378e8da289a2744407ee8cef72 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 90b6edee7de886d12ccc94f42adcefbb4abc6acb..0e86c2fca0697ff05fec7033dd9907378758a012 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 fe1dad584e3a69db63b4bdca7dfa1a78af2324ab..9dadb54e246d211f09fe77dacfa35316f8e6d436 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 95ad1b3a0b77a526af57318562b09fcf77adddee..8b7c08da51b4059dd8d3f71d53760121687b27fe 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<String, JSHttpClient> = ConcurrentHashMap(); val httpClient: ManagedHttpClient get() = _client; val httpClientAuth: ManagedHttpClient get() = _clientAuth; + val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers; + + fun registerHttpClient(client: JSHttpClient) { + synchronized(_clientOthers) { + _clientOthers.put(client.clientId, client); + } + } private val _runtimeLock = Object(); var _runtime : V8Runtime? = null; 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 68a742027d3a964610b0c38674bf78ca882bfbde..937e2d23bcb05272e4c9b71886f38fa7bdd54eab 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<String, String>(); + @Transient + private val _clientId: String?; + + @V8Property + fun clientId(): String? { + return _clientId; + } + constructor(pack: PackageHttp, baseClient: ManagedHttpClient): super() { _package = pack; _client = baseClient; + _clientId = if(_client is JSHttpClient) _client.clientId else null; } @V8Function 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 48f685902ce8eb110b1065f714f3f8fa117216c7..979a679add16396015fa90f159d875ec563e912f 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<HistoryFragment>() }), ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }), ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }), + ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }), ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, { val c = it.context ?: return@ButtonDefinition; Logger.i(TAG, "settings preventPictureInPicture()"); 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 a28e8016cf8196edef8fd4513e7230c6a7c95f47..3af3f01d8241678a7466ded0850616df48602b86 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 0000000000000000000000000000000000000000..f77df2cd8fed1994582f5d1e718ca9c873f3dfd3 --- /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<IPlatformChannel> = arrayListOf(); + private val _disabledCreators: ArrayList<IPlatformChannel> = arrayListOf(); + private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf(); + private val _disabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf(); + + private val _containerEnabled: LinearLayout; + private val _containerDisabled: LinearLayout; + + private val _recyclerCreatorsEnabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>; + private val _recyclerCreatorsDisabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>; + + private val _overlay: FrameLayout; + + private var _group: SubscriptionGroup? = null; + + constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) { + inflate(context, R.layout.fragment_subscriptions_group, this); + _fragment = fragment; + + _overlay = findViewById(R.id.overlay); + _searchBar = findViewById(R.id.search_bar); + _textGroupTitleContainer = findViewById(R.id.text_group_title_container); + _textGroupTitle = findViewById(R.id.text_group_title); + _imageGroup = findViewById(R.id.image_group); + _imageGroupBackground = findViewById(R.id.group_image_background); + _buttonEditImage = findViewById(R.id.button_edit_image); + _textGroupMeta = findViewById(R.id.text_group_meta); + _buttonSettings = findViewById(R.id.button_settings); + _buttonDelete = findViewById(R.id.button_delete); + _imageGroup.setBackgroundColor(Color.GRAY); + + val dp6 = 6.dp(resources); + _imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder() + .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat()) + .build() + + _containerEnabled = findViewById(R.id.container_enabled); + _containerDisabled = findViewById(R.id.container_disabled); + _recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) { + it.itemView.setPadding(0, dp6, 0, dp6); + it.onClick.subscribe { channel -> + disableCreator(channel); + }; + } + _recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) { + it.itemView.setPadding(0, dp6, 0, dp6); + it.onClick.subscribe { channel -> + enableCreator(channel); + }; + } + _recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply { + this.orientation = LinearLayoutManager.VERTICAL; + }; + _recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply { + this.orientation = LinearLayoutManager.VERTICAL; + }; + + _textGroupTitleContainer.setOnClickListener { + _group?.let { editName(it) }; + }; + _textGroupMeta.setOnClickListener { + _group?.let { editName(it) }; + }; + _imageGroup.setOnClickListener { + _group?.let { editImage(it) }; + }; + _buttonEditImage.setOnClickListener { + _group?.let { editImage(it) } + }; + _buttonSettings.setOnClickListener { + + } + _buttonDelete.setOnClickListener { + _group?.let { + StateSubscriptionGroups.instance.deleteSubscriptionGroup(it.id); + }; + fragment.close(true); + } + _buttonSettings.visibility = View.GONE; + + _searchBar.onSearchChanged.subscribe { + filterCreators(); + } + + setGroup(null); + } + + fun save() { + _group?.let { + StateSubscriptionGroups.instance.updateSubscriptionGroup(it); + }; + } + + fun editName(group: SubscriptionGroup) { + val editView = SlideUpMenuTextInput(context, "Group name"); + editView.text = group.name; + UISlideOverlays.showOverlay(_overlay, "Edit name", "Save", { + editView.deactivate(); + val text = editView.text; + if(!text.isNullOrEmpty()) { + group.name = text; + _textGroupTitle.text = text; + save(); + } + }, editView).onCancel.subscribe { + editView.deactivate(); + } + editView.activate(); + } + fun editImage(group: SubscriptionGroup) { + val overlay = ImageVariableOverlay(context, _enabledCreators.map { it.url }); + _overlay.removeAllViews(); + _overlay.addView(overlay); + _overlay.alpha = 0f + _overlay.visibility = View.VISIBLE; + _overlay.animate().alpha(1f).setDuration(300).start(); + overlay.onSelected.subscribe { + group.image = it; + it.setImageView(_imageGroup); + it.setImageView(_imageGroupBackground); + save(); + }; + overlay.onClose.subscribe { + _overlay.visibility = View.GONE; + overlay.removeAllViews(); + } + } + + + fun setGroup(group: SubscriptionGroup?) { + _group = group; + _textGroupTitle.text = group?.name; + + val image = group?.image; + if(image != null) { + image.setImageView(_imageGroupBackground); + image.setImageView(_imageGroup); + } + else { + _imageGroupBackground.setImageResource(0); + _imageGroup.setImageResource(0); + } + updateMeta(); + reloadCreators(group); + } + + @SuppressLint("NotifyDataSetChanged") + private fun reloadCreators(group: SubscriptionGroup?) { + _enabledCreators.clear(); + _disabledCreators.clear(); + + if(group != null) { + val urls = group.urls.toList(); + val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel } + _enabledCreators.addAll(subs.filter { urls.contains(it.url) }); + _disabledCreators.addAll(subs.filter { !urls.contains(it.url) }); + } + filterCreators(); + } + + private fun filterCreators() { + val query = _searchBar.textSearch.text.toString().lowercase(); + val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) }; + val filteredDisabled = _disabledCreators.filter { it.name.lowercase().contains(query) }; + + //Optimize + _enabledCreatorsFiltered.clear(); + _enabledCreatorsFiltered.addAll(filteredEnabled); + _disabledCreatorsFiltered.clear(); + _disabledCreatorsFiltered.addAll(filteredDisabled); + + _recyclerCreatorsEnabled.notifyContentChanged(); + _recyclerCreatorsDisabled.notifyContentChanged(); + } + + private fun enableCreator(channel: IPlatformChannel) { + val index = _disabledCreatorsFiltered.indexOf(channel); + if (index >= 0) { + _disabledCreators.remove(channel) + _disabledCreatorsFiltered.remove(channel); + _recyclerCreatorsDisabled.adapter.notifyItemRangeRemoved(index); + + _enabledCreators.add(channel); + _enabledCreatorsFiltered.add(channel); + _recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreatorsFiltered.size - 1); + + _group?.let { + if(!it.urls.contains(channel.url)) { + it.urls.add(channel.url); + save(); + } + } + updateMeta(); + } + } + private fun disableCreator(channel: IPlatformChannel) { + val index = _enabledCreatorsFiltered.indexOf(channel); + if (index >= 0) { + _enabledCreators.remove(channel) + _enabledCreatorsFiltered.removeAt(index); + _recyclerCreatorsEnabled.adapter.notifyItemRangeRemoved(index); + + _disabledCreators.add(channel); + _disabledCreatorsFiltered.add(channel); + _recyclerCreatorsDisabled.adapter.notifyItemInserted(_disabledCreatorsFiltered.size - 1); + + _group?.let { + it.urls.remove(channel.url); + save(); + } + updateMeta(); + } + } + + private fun updateMeta() { + _textGroupMeta.text = "${_enabledCreators.size} creators"; + } + } +} \ 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 0000000000000000000000000000000000000000..52ed90ddbde22112f6b5ed258b25059106f0a3b7 --- /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<SubscriptionGroup> = arrayListOf(); + private var _list: AnyAdapterView<SubscriptionGroup, SubscriptionGroupListViewHolder>? = null; + private var _overlay: FrameLayout? = null; + + override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = inflater.inflate(R.layout.fragment_subscriptions_group_list, container, false); + _overlay = view.findViewById(R.id.overlay); + val recycler = view.findViewById<RecyclerView>(R.id.list); + val callback = ItemMoveCallback(); + _touchHelper = ItemTouchHelper(callback); + _touchHelper?.attachToRecyclerView(recycler); + + _subs.clear(); + _subs.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups().sortedBy { it.priority }); + _list = recycler.asAny(_subs, RecyclerView.VERTICAL){ + it.onClick.subscribe { + navigate<SubscriptionGroupFragment>(it); + }; + it.onSettings.subscribe { + + }; + it.onDelete.subscribe { group -> + val loc = _subs.indexOf(group); + _subs.remove(group); + _list?.adapter?.notifyItemRangeRemoved(loc); + StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id); + }; + it.onDragDrop.subscribe { + _touchHelper?.startDrag(it); + }; + }; + + callback.onRowMoved.subscribe(::groupMoved); + return view; + } + + private fun groupMoved(fromPosition: Int, toPosition: Int) { + Logger.i("SubscriptionGroupListFragment", "Moved ${fromPosition} to ${toPosition}"); + synchronized(_subs) { + if (fromPosition < toPosition) { + for (i in fromPosition until toPosition) { + Collections.swap(_subs, i, i + 1) + } + } else { + for (i in fromPosition downTo toPosition + 1) { + Collections.swap(_subs, i, i - 1) + } + } + } + _list?.adapter?.notifyItemMoved(fromPosition, toPosition); + + synchronized(_subs) { + for(i in 0 until _subs.size) { + val sub = _subs[i]; + if(sub.priority != i) { + sub.priority = i; + StateSubscriptionGroups.instance.updateSubscriptionGroup(sub, true); + } + } + } + } + + + override fun onShownWithView(parameter: Any?, isBack: Boolean) { + super.onShownWithView(parameter, isBack); + + updateGroups(); + + StateSubscriptionGroups.instance.onGroupsChanged.subscribe(this) { + updateGroups(); + } + + if(topBar is AddTopBarFragment) + (topBar as AddTopBarFragment).onAdd.subscribe { + _overlay?.let { + UISlideOverlays.showCreateSubscriptionGroup(it) + } + }; + } + + private fun updateGroups() { + lifecycleScope.launch(Dispatchers.Main) { + _subs.clear(); + _subs.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups().sortedBy { it.priority }); + _list?.adapter?.notifyContentChanged(); + } + } + + override fun onHide() { + super.onHide(); + StateSubscriptionGroups.instance.onGroupsChanged.remove(this); + + if(topBar is AddTopBarFragment) + (topBar as AddTopBarFragment).onAdd.remove(this); + } + + override fun onBackPressed(): Boolean { + return false; + } + + companion object { + fun newInstance() = SubscriptionGroupListFragment().apply {} + } +} \ 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 cd5dca38ab3555caf364664e108f684ba694ead8..41578d002cbaba958e2d953eb7b351cfea52b301 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<SubscriptionsFeedFragment> { override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar + private var _subGroup: SubscriptionGroup? = null; + constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) { Logger.i(TAG, "SubscriptionsFeedFragment constructor()"); StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total -> @@ -254,6 +257,18 @@ class SubscriptionsFeedFragment : MainFragment() { layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); }; _subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); }; + _subscriptionBar?.onToggleGroup?.subscribe { g -> + if(g is SubscriptionGroup.Add) + UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer); + else { + _subGroup = g; + loadCache(); //TODO: Proper subset update + } + }; + _subscriptionBar?.onHoldGroup?.subscribe { g -> + if(g !is SubscriptionGroup.Add) + fragment.navigate<SubscriptionGroupFragment>(g); + }; synchronized(_filterLock) { _subscriptionBar?.setToggles( @@ -288,9 +303,15 @@ class SubscriptionsFeedFragment : MainFragment() { override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> { val nowSoon = OffsetDateTime.now().plusMinutes(5); + val filterGroup = _subGroup; return results.filter { val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType); + //TODO: Check against a sub cache + if(filterGroup != null && !filterGroup.urls.contains(it.author.url)) + return@filter false; + + if(it.datetime?.isAfter(nowSoon) == true) { if(!_filterSettings.allowPlanned) return@filter false; 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 1625b1e4969eeca556a2472fe305e970a1665a4d..9ca4aa8e17b229002e604380b79706d0a04ead48 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 1497b52daf4f6ff6af4009b9f9d0050f33161abf..1de1f917c3164b2e727c8dd2238f837b9bb321f9 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 0000000000000000000000000000000000000000..c46aa3b8c2fbe548841cca5d7df65d40b800a6b5 --- /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<String> = mutableListOf(); + var priority: Int = 99; + + constructor(name: String) { + this.name = name; + } + constructor(parent: SubscriptionGroup) { + this.id = parent.id; + this.name = parent.name; + this.image = parent.image; + this.urls = parent.urls; + this.priority = parent.priority; + } + + class Selectable(parent: SubscriptionGroup, isSelected: Boolean = false): SubscriptionGroup(parent) { + var selected: Boolean = isSelected; + } + + class Add: SubscriptionGroup("+") { + init { + urls.add("+"); + } + } +} \ 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 45e8e069a5ac869d251ffd58f65cc53d22728d16..bcf685928e1ffd9f911558fbdfe7e84aa33f8f25 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 8d9ceff642c41973fa57b83996d45b657d5f08a3..7b6628e68acea05818b1955140cbe30e2f7c27a5 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<String, String?>) { + 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 85b16fd01e32b8cf75f840f244fa400f5ae68a51..2f08937d975ffca42c78a5ab7d4a064bc0b46102 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<String, String?>) { val newSettings = HashMap(map); + if(id == StateDeveloper.DEV_ID) + { + val decConfig = StatePlatform.instance.getDevClient()?.config ?: return; + for(setting in decConfig.settings) { + if(!newSettings.containsKey(setting.variableOrName) || newSettings[setting.variableOrName] == null) + newSettings[setting.variableOrName] = setting.default; + } + StateDeveloper.instance.setDevClientSettings(newSettings); + return; + } val plugin = getPlugin(id); if(plugin != null) { 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 0000000000000000000000000000000000000000..f77e2fad53229002811c0a55cce99695cc8e7770 --- /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<SubscriptionGroup>("subscription_groups") + .withUnique { it.id } + .load(); + + val onGroupsChanged = Event0(); + + fun getSubscriptionGroup(id: String): SubscriptionGroup? { + return _subGroups.findItem { it.id == id }; + } + fun getSubscriptionGroups(): List<SubscriptionGroup> { + return _subGroups.getItems(); + } + fun updateSubscriptionGroup(subGroup: SubscriptionGroup, preventNotify: Boolean = false) { + _subGroups.save(subGroup); + if(!preventNotify) + onGroupsChanged.emit(); + } + fun deleteSubscriptionGroup(id: String){ + val group = getSubscriptionGroup(id); + if(group != null) { + _subGroups.delete(group); + onGroupsChanged.emit(); + } + } + + + companion object { + const val TAG = "StateSubscriptionGroups"; + const val VERSION = 1; + + private var _instance : StateSubscriptionGroups? = null; + val instance : StateSubscriptionGroups + get(){ + if(_instance == null) + _instance = StateSubscriptionGroups(); + return _instance!!; + }; + + fun finish() { + _instance?.let { + _instance = null; + } + } + } +} \ 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 2ff1e01fb53cc13b77c4a73ec4b6a56d95e7891c..db295aa38f2eb70f217671ca1ba9bc1a83e00aa2 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<I, T>(view: RecyclerView, adapter: BaseAnyAdapter<I, T, T>, where T : AnyAdapter.AnyViewHolder<I>{ companion object { + /* inline fun <I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAny(list: List<I>, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView<I, T> { return asAny(ArrayList(list), orientation, reversed, onCreate); - } + }*/ inline fun <I, reified T : AnyAdapter.AnyViewHolder<I>> RecyclerView.asAny(list: ArrayList<I>, orientation: Int = RecyclerView.VERTICAL, reversed: Boolean = false, noinline onCreate: ((T)->Unit)? = null): AnyAdapterView<I, T> { return AnyAdapterView(this, AnyAdapter.create(list, onCreate), orientation, reversed); } 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 0000000000000000000000000000000000000000..77cf431e542ffb08529c14cc4b507c2291166e76 --- /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<String>(); + + constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { + inflate(context, R.layout.view_search_bar, this); + + textSearch = findViewById(R.id.edit_search) + buttonClear = findViewById(R.id.button_clear_search) + + buttonClear.setOnClickListener { textSearch.text = "" }; + textSearch.addTextChangedListener { + onSearchChanged.emit(it.toString()); + }; + } +} \ 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 225c498b535291e4ca9f18db672dd82fbbd72457..d5ca59f0611a54858af3edab9451021d85f2b5a4 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<I, T : AnyAdapter.AnyViewHolder<I>, IT : ViewHolder> { cb(item); } + @SuppressLint("NotifyDataSetChanged") fun notifyContentChanged() { adapter.notifyDataSetChanged(); } @@ -116,7 +121,6 @@ class AnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>> : BaseAnyAdapter<I, T, T> { private class Adapter<I, T : AnyViewHolder<I>> : RecyclerView.Adapter<T> { private val _parent: AnyAdapter<I, T>; - constructor(parentAdapter: AnyAdapter<I, T>) { _parent = parentAdapter; } 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 0000000000000000000000000000000000000000..b133875318e2ad41cc423abfe963b2b0888efaaf --- /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<IPlatformChannel>( + LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_bar_icon, _viewGroup, false)) { + + private val _creatorThumbnail: CreatorThumbnail; + private val _name: TextView; + private var _channel: IPlatformChannel? = null; + + val onClick = Event1<IPlatformChannel>(); + + private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>( + StateApp.instance.scopeGetter, + { PolycentricCache.instance.getProfileAsync(it) }) + .success { onProfileLoaded(it, true) } + .exception<Throwable> { + Logger.w(TAG, "Failed to load profile.", it); + }; + + init { + _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail); + _name = _view.findViewById(R.id.text_channel_name); + _view.findViewById<LinearLayout>(R.id.root).setOnClickListener { + val s = _channel ?: return@setOnClickListener; + onClick.emit(s); + } + } + + override fun bind(value: IPlatformChannel) { + _taskLoadProfile.cancel(); + + _channel = value; + + _creatorThumbnail.setThumbnail(value.thumbnail, false); + _name.text = value.name; + + val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true); + if (cachedProfile != null) { + onProfileLoaded(cachedProfile, false); + if (cachedProfile.expired) { + _taskLoadProfile.run(value.id); + } + } else { + _taskLoadProfile.run(value.id); + } + } + + private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { + val dp_55 = 55.dp(itemView.context.resources) + val profile = cachedPolycentricProfile?.profile; + val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55) + ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; + + if (avatar != null) { + _creatorThumbnail.setThumbnail(avatar, animate); + } else { + _creatorThumbnail.setThumbnail(_channel?.thumbnail, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate); + } + + if (profile != null) { + _name.text = profile.systemState.username; + } + } + + companion object { + private const val TAG = "CreatorBarViewHolder"; + } +} +class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<SelectableCreatorBarViewHolder.Selectable>( + LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_bar_icon, _viewGroup, false)) { + + private val _creatorThumbnail: CreatorThumbnail; + private val _name: TextView; + private var _channel: Selectable? = null; + + val onClick = Event1<Selectable>(); + + private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>( + StateApp.instance.scopeGetter, + { PolycentricCache.instance.getProfileAsync(it) }) + .success { onProfileLoaded(it, true) } + .exception<Throwable> { + Logger.w(TAG, "Failed to load profile.", it); + }; + + init { + _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail); + _name = _view.findViewById(R.id.text_channel_name); + _view.findViewById<LinearLayout>(R.id.root).setOnClickListener { + val s = _channel ?: return@setOnClickListener; + onClick.emit(s); + } + } + + override fun bind(value: Selectable) { + _taskLoadProfile.cancel(); + + _channel = value; + + if(value.active) + _view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimaryDark, null)) + else + _view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null)) + + _creatorThumbnail.setThumbnail(value.channel.thumbnail, false); + _name.text = value.channel.name; + + val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true); + if (cachedProfile != null) { + onProfileLoaded(cachedProfile, false); + if (cachedProfile.expired) { + _taskLoadProfile.run(value.channel.id); + } + } else { + _taskLoadProfile.run(value.channel.id); + } + } + + private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { + val dp_55 = 55.dp(itemView.context.resources) + val profile = cachedPolycentricProfile?.profile; + val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55) + ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; + + if (avatar != null) { + _creatorThumbnail.setThumbnail(avatar, animate); + } else { + _creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate); + } + + if (profile != null) { + _name.text = profile.systemState.username; + } + } + + companion object { + private const val TAG = "CreatorBarViewHolder"; + } + + data class Selectable(var channel: IPlatformChannel, var active: Boolean) +} \ 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 0000000000000000000000000000000000000000..0f93a5eba28e53e99f87631b188d7e9fd4696a09 --- /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<SubscriptionGroup>( + LayoutInflater.from(_viewGroup.context).inflate(R.layout.view_subscription_group_bar, _viewGroup, false)) { + private var _group: SubscriptionGroup? = null; + + private val _image: ShapeableImageView; + private val _textSubGroup: TextView; + + val onClick = Event1<SubscriptionGroup>(); + val onClickLong = Event1<SubscriptionGroup>(); + + init { + _image = _view.findViewById(R.id.image); + _textSubGroup = _view.findViewById(R.id.text_sub_group); + + val dp6 = 6.dp(_view.resources); + _image.shapeAppearanceModel = ShapeAppearanceModel.builder() + .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat()) + .build() + + _view.setOnClickListener { + _group?.let { + onClick.emit(it); + } + } + _view.setOnLongClickListener { + _group?.let { + onClickLong.emit(it); + } + true; + } + } + + override fun bind(value: SubscriptionGroup) { + _group = value; + val img = value.image; + if(img != null) + img.setImageView(_image) + else { + _image.setImageResource(0); + + if(value is SubscriptionGroup.Add) + _image.setBackgroundColor(Color.DKGRAY); + } + _textSubGroup.text = value.name; + + if(value is SubscriptionGroup.Selectable && value.selected) + _view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null)); + else + _view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null)); + } + + companion object { + private const val TAG = "SubscriptionGroupBarViewHolder"; + } +} \ 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 0000000000000000000000000000000000000000..19fe8a3071dd0bfdd3d4b39043ce81ea31cc4146 --- /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<SubscriptionGroup>( + LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_subscription_group, _viewGroup, false)) { + private var _group: SubscriptionGroup? = null; + + private val _thumb: ImageView; + private val _image: ShapeableImageView; + private val _textSubGroup: TextView; + private val _textSubGroupMeta: TextView; + + private val _buttonSettings: ImageButton; + private val _buttonDelete: ImageButton; + + val onClick = Event1<SubscriptionGroup>(); + val onSettings = Event1<SubscriptionGroup>(); + val onDelete = Event1<SubscriptionGroup>(); + val onDragDrop = Event1<RecyclerView.ViewHolder>(); + + init { + _thumb = _view.findViewById(R.id.thumb); + _image = _view.findViewById(R.id.image); + _textSubGroup = _view.findViewById(R.id.text_sub_group); + _textSubGroupMeta = _view.findViewById(R.id.text_sub_group_meta); + _buttonSettings = _view.findViewById(R.id.button_settings); + _buttonDelete = _view.findViewById(R.id.button_trash); + + val dp6 = 6.dp(_view.resources); + _image.shapeAppearanceModel = ShapeAppearanceModel.builder() + .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat()) + .build() + + _view.setOnClickListener { + _group?.let { + onClick.emit(it); + } + } + _thumb.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + onDragDrop.emit(this); + } + false + }; + _buttonSettings.setOnClickListener { + _group?.let { + onSettings.emit(it); + }; + } + _buttonDelete.setOnClickListener { + _group?.let { + onDelete.emit(it); + }; + } + } + + override fun bind(value: SubscriptionGroup) { + _group = value; + val img = value.image; + if(img != null) + img.setImageView(_image) + else { + _image.setImageResource(0); + + if(value is SubscriptionGroup.Add) + _image.setBackgroundColor(Color.DKGRAY); + } + _textSubGroup.text = value.name; + _textSubGroupMeta.text = "${value.urls.size} subscriptions"; + + if(value is SubscriptionGroup.Selectable && value.selected) + _view.setBackgroundColor(_view.context.resources.getColor(R.color.colorPrimary, null)); + else + _view.setBackgroundColor(_view.context.resources.getColor(R.color.transparent, null)); + } + + companion object { + private const val TAG = "SubscriptionGroupBarViewHolder"; + } +} \ 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 0000000000000000000000000000000000000000..0971d577e219f8e1a909d41953bf01caf12e2b57 --- /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<PresetImage, PresetViewHolder>; + private val _recyclerCreators: AnyAdapterView<SelectableCreatorBarViewHolder.Selectable, SelectableCreatorBarViewHolder>; + + private val _creators: ArrayList<SelectableCreatorBarViewHolder.Selectable> = arrayListOf(); + private val _presets: ArrayList<PresetImage> = + ArrayList(PresetImages.images.map { PresetImage(it.value, it.key, false) }); + + private var _selected: ImageVariable? = null; + private var _selectedFile: String? = null; + + val onSelected = Event1<ImageVariable>(); + val onClose = Event0(); + + constructor(context: Context, creatorFilters: List<String>? = null): super(context) { + val subs = StateSubscriptions.instance.getSubscriptions(); + if(creatorFilters != null) { + _creators.addAll(subs + .filter { creatorFilters.contains(it.channel.url) } + .map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) }); + } + else + _creators.addAll(subs + .map { SelectableCreatorBarViewHolder.Selectable(it.channel, false) }); + _recyclerCreators.notifyContentChanged(); + } + constructor(context: Context, attrs: AttributeSet?): super(context, attrs) { } + init { + inflate(context, R.layout.overlay_image_variable, this); + _topbar = findViewById(R.id.topbar); + _buttonGallery = findViewById(R.id.button_gallery); + _imageGallerySelected = findViewById(R.id.gallery_selected); + _imageGallerySelectedContainer = findViewById(R.id.gallery_selected_container); + _buttonSelect = findViewById(R.id.button_select); + _recyclerPresets = findViewById<RecyclerView>(R.id.recycler_presets).asAny(_presets, RecyclerView.HORIZONTAL) { + it.onClick.subscribe { + _selected = ImageVariable.fromPresetName(it.name); + updateSelected(); + }; + }; + val dp6 = 6.dp(resources); + _recyclerCreators = findViewById<RecyclerView>(R.id.recycler_creators).asAny(_creators, RecyclerView.HORIZONTAL) { creatorView -> + creatorView.itemView.setPadding(0, dp6, 0, dp6); + creatorView.onClick.subscribe { + if(it.channel.thumbnail == null) { + UIDialogs.toast(context, "No thumbnail found"); + return@subscribe; + } + _selected = ImageVariable(it.channel.thumbnail); + updateSelected(); + }; + }; + _recyclerCreators.view.layoutManager = GridLayoutManager(context, 5).apply { + this.orientation = LinearLayoutManager.VERTICAL; + }; + + _buttonGallery.onClick.subscribe { + val context = StateApp.instance.contextOrNull; + if(context is IWithResultLauncher && context is MainActivity) { + ImagePicker.with(context) + .compress(512) + .maxResultSize(750, 500) + .createIntent { + context.launchForResult(it, 888) { + if(it.resultCode == Activity.RESULT_OK) { + cleanupLastFile(); + val fileUri = it.data?.data; + if(fileUri != null) { + val file = fileUri.toFile(); + val ext = file.extension; + val persistFile = StateApp.instance.getPersistFile(ext); + file.copyTo(persistFile); + _selectedFile = persistFile.toUri().toString(); + _selected = ImageVariable(_selectedFile); + updateSelected(); + } + } + }; + }; + } + }; + _imageGallerySelectedContainer.setOnClickListener { + if(_selectedFile != null) { + _selected = ImageVariable(_selectedFile); + updateSelected(); + } + } + _buttonSelect.setOnClickListener { + _selected?.let { + select(it); + } + }; + _topbar.onClose.subscribe { + onClose.emit(); + } + updateSelected(); + } + + fun updateSelected() { + val id = _selected?.resId; + val name = _selected?.presetName; + val url = _selected?.url; + _presets.forEach { p -> p.active = p.name == name }; + _recyclerPresets.notifyContentChanged(); + _creators.forEach { p -> p.active = p.channel.thumbnail == url }; + _recyclerCreators.notifyContentChanged(); + + if(_selectedFile != null) { + _imageGallerySelectedContainer.visibility = View.VISIBLE; + Glide.with(_imageGallerySelected) + .load(_selectedFile) + .into(_imageGallerySelected); + } + else + _imageGallerySelectedContainer.visibility = View.GONE; + + if(_selected?.url == _selectedFile) + _imageGallerySelectedContainer.setBackgroundColor(resources.getColor(R.color.colorPrimary, null)); + else + _imageGallerySelectedContainer.setBackgroundColor(resources.getColor(R.color.transparent, null)); + + if(_selected != null) + _buttonSelect.alpha = 1f; + else + _buttonSelect.alpha = 0.5f; + } + fun cleanupLastFile() { + _selectedFile?.let { + val file = File(it); + if(file.exists()) + file.delete(); + _selectedFile = null; + } + } + + + fun select(variable: ImageVariable) { + if(_selected?.url != _selectedFile) + cleanupLastFile(); + onSelected.emit(variable); + onClose.emit(); + } + + class PresetViewHolder(viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<PresetImage>(LinearLayout(viewGroup.context)) { + private val view = _view as LinearLayout; + private val imageView = ShapeableImageView(viewGroup.context); + + private var value: PresetImage = PresetImage(0, "", false); + + val onClick = Event1<PresetImage>(); + init { + view.addView(imageView); + val dp2 = 2.dp(viewGroup.context.resources); + val dp6 = 6.dp(viewGroup.context.resources); + view.setPadding(dp2, dp2, dp2, dp2); + imageView.setOnClickListener { + onClick.emit(value); + } + imageView.layoutParams = LinearLayout.LayoutParams(110.dp(viewGroup.context.resources), 70.dp(viewGroup.context.resources)).apply { + //this.rightMargin = dp6 + } + imageView.scaleType = ImageView.ScaleType.CENTER_CROP + imageView.shapeAppearanceModel = ShapeAppearanceModel.builder() + .setAllCorners(CornerFamily.ROUNDED, dp6.toFloat()) + .build() + } + + override fun bind(value: PresetImage) { + imageView.setImageResource(value.id); + this.value = value; + setActive(value.active); + } + + fun setActive(active: Boolean) { + if(active) + _view.setBackgroundColor(view.context.resources.getColor(R.color.colorPrimary, null)); + else + _view.setBackgroundColor(view.context.resources.getColor(R.color.transparent, null)); + } + } + + data class PresetImage(var id: Int, var name: String, var active: Boolean); +} \ 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 ca4a39a98e89707b9064ff37816ebcf728b346a1..9a53fe44d8bfad672d33f931ce5433ffc5560bf1 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 d001cdc56e49ef78d26cda28b4864d908c08e171..a456cba007270128deff160e86b602fb55160ea9 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<Subscription, SubscriptionBarViewHolder>? = null; + private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder> private val _tagsContainer: LinearLayout; + private val _groups: ArrayList<SubscriptionGroup>; + private var _group: SubscriptionGroup? = null; + val onClickChannel = Event1<SerializedChannel>(); + val onToggleGroup = Event1<SubscriptionGroup?>(); + val onHoldGroup = Event1<SubscriptionGroup>(); + override fun onAttachedToWindow() { + super.onAttachedToWindow(); + StateSubscriptionGroups.instance.onGroupsChanged.subscribe(this) { + findViewTreeLifecycleOwner()?.lifecycleScope?.launch(Dispatchers.Main) { + reloadGroups(); + } + } + } + override fun onDetachedFromWindow() { + super.onDetachedFromWindow(); + StateSubscriptionGroups.instance.onGroupsChanged.remove(this); + } constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { inflate(context, R.layout.view_subscription_bar, this); - val subscriptions = StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds }; + val subscriptions = ArrayList(StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds }); _adapterView = findViewById<RecyclerView>(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) { it.onClick.subscribe { c -> onClickChannel.emit(c.channel); }; }; + _groups = ArrayList(getGroups()); + _subGroups = findViewById<RecyclerView>(R.id.recycler_subgroups).asAny(_groups, orientation = RecyclerView.HORIZONTAL) { + it.onClick.subscribe(::groupClicked); + it.onClickLong.subscribe { g -> + onHoldGroup.emit(g); + } + } _tagsContainer = findViewById(R.id.container_tags); } + private fun groupClicked(g: SubscriptionGroup) { + if(g is SubscriptionGroup.Add) { + onToggleGroup.emit(g); + return; + } + val isSame = _group == g; + _group?.let { + if (it is SubscriptionGroup.Selectable) { + it.selected = false; + val index = _groups.indexOf(it); + if (index >= 0) + _subGroups.notifyContentChanged(index); + } + } + + if(isSame) { + _group = null; + onToggleGroup.emit(null); + } + else { + _group = g; + if(g is SubscriptionGroup.Selectable) + g.selected = true; + _subGroups.notifyContentChanged(_groups.indexOf(g)); + onToggleGroup.emit(g); + } + } + + private fun reloadGroups() { + val results = getGroups(); + _groups.clear(); + _groups.addAll(results); + _subGroups.notifyContentChanged(); + } + private fun getGroups(): List<SubscriptionGroup> { + return if(Settings.instance.subscriptions.showSubscriptionGroups) + (StateSubscriptionGroups.instance.getSubscriptionGroups() + .sortedBy { it.priority } + .map { SubscriptionGroup.Selectable(it, it.id == _group?.id) } + + listOf(SubscriptionGroup.Add())); + else listOf(); + } + fun setToggles(vararg buttons: Toggle) { _tagsContainer.removeAllViews(); 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 abe86c622f2b0506b1701ac3566cfcd8140ebf68..25100c2f4b45d1be6344afe9494fd8c20a05399d 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 d153c4405606ec9488baebb7f0bc39d73ef41f1a..4c99ccb9c47bef960eac2db5538e2f0492f253e8 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<String> contentTypePredicate, boolean keepPostFor302Redirects, - @Nullable JSRequestModifier requestModifier) { + @Nullable IRequestModifier requestModifier) { super(/* isNetwork= */ true); this.userAgent = userAgent; this.connectTimeoutMillis = connectTimeoutMillis; @@ -574,8 +576,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource { String requestUrl = url.toString(); if (requestModifier != null) { - JSRequestModifier.IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders); - requestUrl = result.getUrl(); + IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders); + String modifiedUrl = result.getUrl(); + requestUrl = (modifiedUrl != null) ? modifiedUrl : requestUrl; requestHeaders = result.getHeaders(); } 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 0000000000000000000000000000000000000000..1eafcfd944fe0d8f8676750d8324a8728771bd93 --- /dev/null +++ b/app/src/main/res/drawable/background_primary_rounded_2dp.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/colorPrimary" /> + <corners android:radius="1dp" /> + <padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" /> +</shape> \ 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 0000000000000000000000000000000000000000..27d3ffc633997a267cd6db78d7b1a95f2fbbe39c --- /dev/null +++ b/app/src/main/res/drawable/ic_gallery.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M164.62,720Q137.96,720 118.98,701.02Q100,682.04 100,655.39L100,304.61Q100,277.96 118.98,258.98Q137.96,240 164.62,240L515.39,240Q542.04,240 561.02,258.98Q580,277.96 580,304.61L580,655.39Q580,682.04 561.02,701.02Q542.04,720 515.39,720L164.62,720ZM692.69,440Q678.39,440 669.19,430.81Q660,421.61 660,407.31L660,272.69Q660,258.38 669.19,249.19Q678.39,240 692.69,240L827.31,240Q841.62,240 850.81,249.19Q860,258.38 860,272.69L860,407.31Q860,421.61 850.81,430.81Q841.62,440 827.31,440L692.69,440ZM700,400L820,400L820,280L700,280L700,400ZM164.62,680L515.39,680Q526.15,680 533.08,673.08Q540,666.15 540,655.39L540,304.61Q540,293.85 533.08,286.92Q526.15,280 515.39,280L164.62,280Q153.85,280 146.92,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 146.92,673.08Q153.85,680 164.62,680ZM187.69,596.15L492.31,596.15L395,466.15L320,566.15L265,493.15L187.69,596.15ZM692.69,720Q678.39,720 669.19,710.81Q660,701.62 660,687.31L660,552.69Q660,538.39 669.19,529.19Q678.39,520 692.69,520L827.31,520Q841.62,520 850.81,529.19Q860,538.39 860,552.69L860,687.31Q860,701.62 850.81,710.81Q841.62,720 827.31,720L692.69,720ZM700,680L820,680L820,560L700,560L700,680ZM140,680Q140,680 140,673.08Q140,666.15 140,655.39L140,304.61Q140,293.85 140,286.92Q140,280 140,280L140,280Q140,280 140,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 140,673.08Q140,680 140,680L140,680ZM700,400L700,280L700,280L700,400L700,400ZM700,680L700,560L700,560L700,680L700,680Z"/> +</vector> 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 0000000000000000000000000000000000000000..6ff22bb6d6e45182a78375714478c5f74906fc01 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 0000000000000000000000000000000000000000..00dfb956d01715116212876f2e92b22a1f7c5b68 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 0000000000000000000000000000000000000000..bd6aeaa06b210cd64c3a0c36eee6f0e197fd8958 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 0000000000000000000000000000000000000000..e0428a2f44a45f198a9306c3d2c91a5d69f642e8 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 0000000000000000000000000000000000000000..d33568c0d0b3a4dcfe6793b55f24deb94d283a35 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 0000000000000000000000000000000000000000..cf728852673abd8fdab6321d457129a1cb64d097 --- /dev/null +++ b/app/src/main/res/layout/fragment_subscriptions_group.xml @@ -0,0 +1,235 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:animateLayoutChanges="true"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="150dp" + android:background="#AAAAAA"> + <ImageView + android:id="@+id/group_image_background" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:src="@drawable/xp_book" + android:scaleType="centerCrop" /> + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#AA000000"> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintRight_toRightOf="parent" + android:orientation="horizontal"> + <ImageButton + android:id="@+id/button_delete" + android:layout_width="50dp" + android:layout_height="50dp" + android:layout_marginLeft="5dp" + android:layout_marginRight="0dp" + android:src="@drawable/ic_trash" + app:tint="#CC0000" + android:padding="10dp" + android:background="@color/transparent" + android:visibility="visible" + android:scaleType="fitCenter" /> + + <ImageButton + android:id="@+id/button_settings" + android:layout_width="50dp" + android:layout_height="50dp" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" + android:src="@drawable/ic_settings" + android:padding="10dp" + android:background="@color/transparent" + android:visibility="visible" + android:scaleType="fitCenter" /> + </LinearLayout> + + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/image_group" + android:layout_width="110dp" + android:layout_height="70dp" + android:adjustViewBounds="true" + app:circularflow_defaultRadius="10dp" + android:layout_marginLeft="30dp" + android:src="@drawable/xp_book" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:scaleType="centerCrop" + app:layout_constraintBottom_toBottomOf="parent" /> + + <LinearLayout + android:id="@+id/button_edit_image" + android:layout_width="30dp" + android:layout_height="30dp" + app:layout_constraintBottom_toTopOf="@id/image_group" + app:layout_constraintLeft_toRightOf="@id/image_group" + android:layout_marginLeft="-15dp" + android:layout_marginBottom="-15dp" + android:background="@drawable/background_pill"> + <ImageButton + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="5dp" + android:clickable="false" + android:scaleType="fitCenter" + android:background="@color/transparent" + android:src="@drawable/ic_edit"/> + </LinearLayout> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintLeft_toRightOf="@id/image_group" + app:layout_constraintTop_toTopOf="@id/image_group" + app:layout_constraintBottom_toBottomOf="@id/image_group" + app:layout_constraintRight_toRightOf="parent" + android:layout_marginStart="25dp" + android:layout_marginTop="10dp" + android:orientation="vertical"> + <LinearLayout + android:id="@+id/text_group_title_container" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView + android:id="@+id/text_group_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@font/inter_bold" + android:textSize="15dp" + android:text="News" /> + <ImageButton + android:layout_width="20dp" + android:layout_height="20dp" + android:padding="2dp" + android:layout_marginStart="5dp" + android:layout_marginBottom="-5dp" + android:scaleType="fitCenter" + android:background="@color/transparent" + android:src="@drawable/ic_edit"/> + </LinearLayout> + <TextView + android:id="@+id/text_group_meta" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@font/inter_light" + android:textSize="12dp" + android:text="42 creators" /> + </LinearLayout> + </androidx.constraintlayout.widget.ConstraintLayout> + </FrameLayout> + + <com.futo.platformplayer.views.SearchView + android:id="@+id/search_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="match_parent"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="0dp" + android:paddingEnd="0dp"> + <LinearLayout + android:id="@+id/container_enabled" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16dp" + android:layout_marginLeft="20dp" + android:layout_marginRight="20dp" + android:textColor="@color/white" + android:fontFamily="@font/inter_light" + android:text="@string/enabled" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="12dp" + android:layout_marginLeft="20dp" + android:layout_marginRight="20dp" + android:textColor="@color/gray_ac" + android:fontFamily="@font/inter_extra_light" + android:text="@string/these_creators_in_group" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_creators_enabled" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/container_disabled" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_marginTop="10dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16dp" + android:layout_marginLeft="20dp" + android:layout_marginRight="20dp" + android:textColor="@color/white" + android:fontFamily="@font/inter_light" + android:text="@string/disabled" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="12dp" + android:layout_marginLeft="20dp" + android:layout_marginRight="20dp" + android:textColor="@color/gray_ac" + android:fontFamily="@font/inter_extra_light" + android:text="@string/these_creators_not_in_group" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_creators_disabled" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginTop="10dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" /> + </LinearLayout> + + </LinearLayout> + </ScrollView> + </LinearLayout> + + <FrameLayout + android:id="@+id/overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" /> + +</androidx.constraintlayout.widget.ConstraintLayout> 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 0000000000000000000000000000000000000000..0c0b91b1fcf140cade61360c187ef478736e8e44 --- /dev/null +++ b/app/src/main/res/layout/fragment_subscriptions_group_list.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + </androidx.recyclerview.widget.RecyclerView> + <FrameLayout + android:id="@+id/overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" /> +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ 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 0000000000000000000000000000000000000000..d7c1d7bcfcc1909eb916bd2b7712fc4df4655b83 --- /dev/null +++ b/app/src/main/res/layout/list_subscription_group.xml @@ -0,0 +1,99 @@ +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="70dp" + android:orientation="vertical" + android:gravity="center_horizontal" + android:padding="2dp" + android:layout_margin="2dp" + android:clickable="true" + android:id="@+id/root"> + <ImageView + android:id="@+id/thumb" + android:layout_width="50dp" + android:layout_height="match_parent" + android:padding="12dp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:src="@drawable/ic_dragdrop_white" + android:background="@color/transparent" + /> + + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/image" + android:layout_width="75dp" + android:layout_height="50dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toRightOf="@id/thumb" + android:layout_marginLeft="10dp" + android:scaleType="centerCrop" + android:src="@drawable/xp_book" /> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + app:layout_constraintLeft_toRightOf="@id/image" + app:layout_constraintRight_toLeftOf="@id/buttons" + android:layout_marginLeft="10dp" + android:gravity="center" + android:orientation="vertical"> + <TextView + android:id="@+id/text_sub_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" + android:maxLines="2" + android:ellipsize="end" + android:textSize="12dp" + android:textColor="@color/white" + android:textAlignment="textStart" + android:text="News" /> + <TextView + android:id="@+id/text_sub_group_meta" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" + android:maxLines="2" + android:ellipsize="end" + android:textSize="10dp" + android:textColor="@color/gray_ac" + android:textAlignment="textStart" + android:text="News" /> + </LinearLayout> + <LinearLayout + android:id="@+id/buttons" + android:layout_width="wrap_content" + android:layout_height="match_parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:orientation="horizontal"> + <ImageButton + android:id="@+id/button_trash" + android:layout_width="50dp" + android:layout_height="match_parent" + android:layout_marginRight="10dp" + android:padding="10dp" + android:scaleType="fitCenter" + android:background="@color/transparent" + app:tint="@color/pastel_red" + android:src="@drawable/ic_trash" + /> + <ImageButton + android:id="@+id/button_settings" + android:layout_width="50dp" + android:layout_height="match_parent" + android:padding="10dp" + android:scaleType="fitCenter" + android:background="@color/transparent" + android:src="@drawable/ic_settings" + android:visibility="gone" + /> + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> 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 0000000000000000000000000000000000000000..b89d7fd458fdedb293fdff701541dabb160f57d8 --- /dev/null +++ b/app/src/main/res/layout/overlay_image_variable.xml @@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:background="@color/black" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <com.futo.platformplayer.views.overlays.OverlayTopbar + android:id="@+id/topbar" + android:layout_width="match_parent" + android:layout_height="40dp" + app:title="Select an Image" + app:metadata="" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" /> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintTop_toBottomOf="@id/topbar" + app:layout_constraintBottom_toTopOf="@id/container_select" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center_horizontal"> + + <LinearLayout + android:id="@+id/gallery_selected_container" + android:layout_width="150dp" + android:layout_height="100dp" + android:gravity="center_horizontal" + android:padding="2dp" + android:layout_marginTop="10dp" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp"> + <ImageView + android:id="@+id/gallery_selected" + android:layout_width="150dp" + android:layout_height="100dp" + android:scaleType="centerCrop" /> + </LinearLayout> + <com.futo.platformplayer.views.buttons.BigButton + android:id="@+id/button_gallery" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="10dp" + app:buttonIcon="@drawable/ic_gallery" + app:buttonText="Open Photo Gallery" + app:buttonSubText="Pick an image from the gallery" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:layout_marginBottom="10dp" + android:orientation="vertical"> + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fontFamily="@font/inter_regular" + android:textSize="20dp" + android:text="Preset" /> + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:textColor="@color/gray_ac" + android:textSize="12dp" + android:text="Pick a preset image" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_presets" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + </androidx.recyclerview.widget.RecyclerView> + + </LinearLayout> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:layout_marginBottom="10dp" + android:orientation="vertical"> + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fontFamily="@font/inter_regular" + android:textSize="20dp" + android:text="Creator" /> + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:textColor="@color/gray_ac" + android:textSize="12dp" + android:text="Pick a creator as group image" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_creators" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + </androidx.recyclerview.widget.RecyclerView> + </LinearLayout> + + </LinearLayout> + </ScrollView> + + <LinearLayout + android:id="@+id/container_select" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent"> + <Button + android:id="@+id/button_select" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/colorPrimary" + android:text="Select" /> + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/view_search_bar.xml b/app/src/main/res/layout/view_search_bar.xml new file mode 100644 index 0000000000000000000000000000000000000000..c913c028de47821ae0317d6edc7192f1419ae67d --- /dev/null +++ b/app/src/main/res/layout/view_search_bar.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="40dp" + android:layout_margin="10dp" + android:orientation="vertical" + android:id="@+id/root"> + + + <EditText + android:id="@+id/edit_search" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="text" + android:imeOptions="actionDone" + android:singleLine="true" + android:hint="Search" + android:paddingEnd="46dp" /> + + <ImageButton + android:id="@+id/button_clear_search" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:paddingStart="18dp" + android:paddingEnd="18dp" + android:layout_gravity="right|center_vertical" + android:visibility="invisible" + android:src="@drawable/ic_clear_16dp" /> + +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/view_subscription_bar.xml b/app/src/main/res/layout/view_subscription_bar.xml index f08bb7dfbd8319b0336790288325513cd640a646..5a11c82aa176bc69c2656713087fb00d89a74f30 100644 --- a/app/src/main/res/layout/view_subscription_bar.xml +++ b/app/src/main/res/layout/view_subscription_bar.xml @@ -23,4 +23,9 @@ android:layout_height="wrap_content" android:orientation="horizontal" /> </ScrollView> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_subgroups" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" /> </LinearLayout> diff --git a/app/src/main/res/layout/view_subscription_group_bar.xml b/app/src/main/res/layout/view_subscription_group_bar.xml new file mode 100644 index 0000000000000000000000000000000000000000..8dc1f27aeeeff7a41215ff07792e02721e450bd5 --- /dev/null +++ b/app/src/main/res/layout/view_subscription_group_bar.xml @@ -0,0 +1,36 @@ +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="78dp" + android:layout_height="54dp" + android:orientation="vertical" + android:gravity="center_horizontal" + android:padding="2dp" + android:layout_margin="2dp" + android:clickable="true" + android:id="@+id/root"> + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" + android:src="@drawable/xp_book" /> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#99000000" + android:gravity="center"> + <TextView + android:id="@+id/text_sub_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" + android:maxLines="2" + android:ellipsize="end" + android:textSize="12dp" + android:textAlignment="center" + android:text="News" /> + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/view_toggle_tag.xml b/app/src/main/res/layout/view_toggle_tag.xml index 0992886c750e328b3b98dc68b87349568dc85340..886f2de5c871e9641686ebeb174c2924a9557c33 100644 --- a/app/src/main/res/layout/view_toggle_tag.xml +++ b/app/src/main/res/layout/view_toggle_tag.xml @@ -2,7 +2,7 @@ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" - android:layout_height="27dp" + android:layout_height="32dp" android:paddingStart="15dp" android:paddingEnd="15dp" android:background="@drawable/background_pill" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da1c3f6edb88118bd435266f6819904d8c596a03..9b50c55294aea350cb0cda7b364795c7f99de278 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,6 +69,8 @@ <string name="discover">Discover</string> <string name="find_new_video_sources_to_add">Find new video sources to add</string> <string name="these_sources_have_been_disabled">These sources have been disabled</string> + <string name="these_creators_in_group">These are the creators that are visible for this group.</string> + <string name="these_creators_not_in_group">These creators are not in this group.</string> <string name="disabled">Disabled</string> <string name="watch_later">Watch Later</string> <string name="create">Create</string> @@ -346,6 +348,9 @@ <string name="allow_full_screen_portrait">Allow fullscreen portrait</string> <string name="background_switch_audio">Switch to Audio in Background</string> <string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string> + <string name="subscription_group_menu">Groups</string> + <string name="show_subscription_group">Show Subscription Groups</string> + <string name="show_subscription_group_description">If subscription groups should be shown above your subscriptions to filter</string> <string name="preview_feed_items">Preview Feed Items</string> <string name="preview_feed_items_description">When the preview feedstyle is used, if items should auto-preview when scrolling over them</string> <string name="log_level">Log Level</string> @@ -564,6 +569,7 @@ <string name="playlist_copied_as_local_playlist">Playlist copied as local playlist</string> <string name="are_you_sure_you_want_to_delete_the_downloaded_videos">Are you sure you want to delete the downloaded videos?</string> <string name="create_new_playlist">Create new playlist</string> + <string name="create_new_subgroup">Create new subscription group</string> <string name="expected_media_content_found">Expected media content, found</string> <string name="failed_to_load_post">Failed to load post.</string> <string name="replies">replies</string>