mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-05 15:49:22 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
d63627bd61
81 changed files with 2474 additions and 169 deletions
|
@ -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.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
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.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
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.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||||
|
|
||||||
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
fun IAudioSource.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());
|
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
|
20
app/src/main/java/com/futo/platformplayer/PresetImages.kt
Normal file
20
app/src/main/java/com/futo/platformplayer/PresetImages.kt
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
class PresetImages {
|
||||||
|
companion object {
|
||||||
|
val images = mapOf<String, Int>(
|
||||||
|
Pair("xp_book", R.drawable.xp_book),
|
||||||
|
Pair("xp_forest", R.drawable.xp_forest),
|
||||||
|
Pair("xp_code", R.drawable.xp_code),
|
||||||
|
Pair("xp_controller", R.drawable.xp_controller),
|
||||||
|
Pair("xp_laptop", R.drawable.xp_laptop)
|
||||||
|
);
|
||||||
|
|
||||||
|
fun getPresetResIdByName(name: String): Int {
|
||||||
|
return images[name] ?: -1;
|
||||||
|
}
|
||||||
|
fun getPresetNameByResId(id: Int): String? {
|
||||||
|
return images.entries.find { it.value == id }?.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -261,20 +261,23 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
return FeedStyle.THUMBNAIL;
|
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;
|
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;
|
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)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var fetchOnAppBoot: Boolean = true;
|
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;
|
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)
|
@DropdownFieldOptionsId(R.array.background_interval)
|
||||||
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
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)
|
@DropdownFieldOptionsId(R.array.thread_count)
|
||||||
var subscriptionConcurrency: Int = 3;
|
var subscriptionConcurrency: Int = 3;
|
||||||
|
|
||||||
|
@ -298,17 +301,17 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
return threadIndexToCount(subscriptionConcurrency);
|
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;
|
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;
|
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;
|
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() {
|
fun clearChannelCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||||
StateCache.instance.clear();
|
StateCache.instance.clear();
|
||||||
|
|
|
@ -3,8 +3,10 @@ package com.futo.platformplayer
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
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.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
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.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
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.RoundButton
|
||||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||||
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
||||||
|
import isDownloadable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -46,7 +52,7 @@ class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "UISlideOverlays";
|
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);
|
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
|
@ -54,6 +60,7 @@ class UISlideOverlays {
|
||||||
onOk.invoke();
|
onOk.invoke();
|
||||||
};
|
};
|
||||||
menu.show();
|
menu.show();
|
||||||
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
||||||
|
@ -78,6 +85,7 @@ class UISlideOverlays {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
}, false),
|
}, false),
|
||||||
|
|
||||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
-1, listOf()),
|
-1, listOf()),
|
||||||
|
@ -96,7 +104,15 @@ class UISlideOverlays {
|
||||||
}, false) else null,
|
}, false) else null,
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
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;
|
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);
|
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 {
|
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>(LoaderView(container.context))
|
val items = arrayListOf<View>(LoaderView(container.context))
|
||||||
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||||
|
@ -512,6 +532,48 @@ class UISlideOverlays {
|
||||||
return overlay;
|
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 {
|
fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
|
||||||
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
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);
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
|
@ -36,6 +36,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
import com.futo.platformplayer.listeners.OrientationManager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
@ -100,6 +101,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
||||||
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
|
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
|
||||||
lateinit var _fragBuy: BuyFragment;
|
lateinit var _fragBuy: BuyFragment;
|
||||||
|
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
||||||
|
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
||||||
|
|
||||||
lateinit var _fragBrowser: BrowserFragment;
|
lateinit var _fragBrowser: BrowserFragment;
|
||||||
|
|
||||||
|
@ -235,6 +238,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
||||||
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
|
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
|
||||||
_fragBuy = BuyFragment.newInstance();
|
_fragBuy = BuyFragment.newInstance();
|
||||||
|
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
||||||
|
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
||||||
|
|
||||||
_fragBrowser = BrowserFragment.newInstance();
|
_fragBrowser = BrowserFragment.newInstance();
|
||||||
|
|
||||||
|
@ -316,6 +321,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
_fragDownloads.topBar = _fragTopBarGeneral;
|
_fragDownloads.topBar = _fragTopBarGeneral;
|
||||||
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
||||||
_fragImportPlaylists.topBar = _fragTopBarImport;
|
_fragImportPlaylists.topBar = _fragTopBarImport;
|
||||||
|
_fragSubGroup.topBar = _fragTopBarNavigation;
|
||||||
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
|
@ -982,6 +989,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
|
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
|
||||||
BrowserFragment::class -> _fragBrowser as T;
|
BrowserFragment::class -> _fragBrowser as T;
|
||||||
BuyFragment::class -> _fragBuy 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");
|
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
|
||||||
Settings.instance.save()
|
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
|
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
|
||||||
TabViewHolderData(buttonDefinition, it.enabled)
|
TabViewHolderData(buttonDefinition, it.enabled)
|
||||||
};
|
});
|
||||||
|
|
||||||
_listTabs = _recyclerTabs.asAny(items) {
|
_listTabs = _recyclerTabs.asAny(items) {
|
||||||
it.onDragDrop.subscribe { vh ->
|
it.onDragDrop.subscribe { vh ->
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
interface IModifierOptions {
|
||||||
|
val applyAuthClient: String?;
|
||||||
|
val applyCookieClient: String?;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
interface IRequest {
|
||||||
|
val url: String?;
|
||||||
|
val headers: Map<String, String>;
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ class DevJSClient : JSClient {
|
||||||
|
|
||||||
val devID: String;
|
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;
|
_devScript = script;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
|
@ -49,7 +49,7 @@ class DevJSClient : JSClient {
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
}
|
}
|
||||||
fun recreate(context: Context): DevJSClient {
|
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 {
|
override fun getCopy(): JSClient {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
@ -67,8 +68,8 @@ open class JSClient : IPlatformClient {
|
||||||
var descriptor: SourcePluginDescriptor
|
var descriptor: SourcePluginDescriptor
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
private val _client: JSHttpClient;
|
private val _httpClient: JSHttpClient;
|
||||||
private val _clientAuth: JSHttpClient?;
|
private val _httpClientAuth: JSHttpClient?;
|
||||||
private var _searchCapabilities: ResultCapabilities? = null;
|
private var _searchCapabilities: ResultCapabilities? = null;
|
||||||
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
||||||
private var _channelCapabilities: ResultCapabilities? = null;
|
private var _channelCapabilities: ResultCapabilities? = null;
|
||||||
|
@ -131,9 +132,9 @@ open class JSClient : IPlatformClient {
|
||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this, null, _captcha);
|
_httpClient = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
|
_plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
|
|
||||||
|
@ -160,9 +161,9 @@ open class JSClient : IPlatformClient {
|
||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this, null, _captcha);
|
_httpClient = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
|
_plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
_plugin.withScript(script);
|
_plugin.withScript(script);
|
||||||
|
@ -181,6 +182,13 @@ open class JSClient : IPlatformClient {
|
||||||
fun getUnderlyingPlugin(): V8Plugin {
|
fun getUnderlyingPlugin(): V8Plugin {
|
||||||
return _plugin;
|
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() {
|
override fun initialize() {
|
||||||
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
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")
|
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
||||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
|
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.getHome()"));
|
plugin.executeTyped("source.getHome()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,7 +300,7 @@ open class JSClient : IPlatformClient {
|
||||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||||
ensureEnabled();
|
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)})"));
|
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)
|
if(!capabilities.hasSearchChannelContents)
|
||||||
throw IllegalStateException("This plugin does not support channel search");
|
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)})"));
|
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")
|
@JSDocsParameter("query", "Query that channels should match")
|
||||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
|
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSChannelPager(config, plugin,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,7 +380,7 @@ open class JSClient : IPlatformClient {
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||||
ensureEnabled();
|
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)})"));
|
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)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
|
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith IJSContentDetails.fromV8(config,
|
return@isBusyWith IJSContentDetails.fromV8(this,
|
||||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -476,13 +484,13 @@ open class JSClient : IPlatformClient {
|
||||||
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||||
return@isBusyWith EmptyPager<IPlatformComment>();
|
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")
|
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
||||||
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
||||||
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return comment.getReplies(this) ?: JSCommentPager(config, plugin,
|
return comment.getReplies(this) ?: JSCommentPager(config, this,
|
||||||
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -501,7 +509,7 @@ open class JSClient : IPlatformClient {
|
||||||
if(!capabilities.hasGetLiveEvents)
|
if(!capabilities.hasGetLiveEvents)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSLiveEventPager(config, plugin,
|
return@isBusyWith JSLiveEventPager(config, this,
|
||||||
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
||||||
|
@ -514,7 +522,7 @@ open class JSClient : IPlatformClient {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if(!capabilities.hasSearchPlaylists)
|
if(!capabilities.hasSearchPlaylists)
|
||||||
throw IllegalStateException("This plugin does not support playlist search");
|
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
|
@JSOptional
|
||||||
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
@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")
|
@JSDocsParameter("url", "Url of playlist")
|
||||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
|
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
|
||||||
ensureEnabled();
|
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
|
@JSOptional
|
||||||
|
|
|
@ -26,17 +26,19 @@ class SourcePluginDescriptor {
|
||||||
@kotlinx.serialization.Transient
|
@kotlinx.serialization.Transient
|
||||||
val onCaptchaChanged = Event0();
|
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.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
this.captchaEncrypted = captchaEncrypted;
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = listOf();
|
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.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
this.captchaEncrypted = captchaEncrypted;
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
|
this.settings = settings ?: hashMapOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSettingsWithDefaults(): HashMap<String, String?> {
|
fun getSettingsWithDefaults(): HashMap<String, String?> {
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.js.internal
|
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.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
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.SourceAuth
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
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.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.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.matchesDomain
|
import com.futo.platformplayer.matchesDomain
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
private val _jsClient: JSClient?;
|
private val _jsClient: JSClient?;
|
||||||
|
@ -14,12 +18,15 @@ class JSHttpClient : ManagedHttpClient {
|
||||||
private val _auth: SourceAuth?;
|
private val _auth: SourceAuth?;
|
||||||
private val _captcha: SourceCaptchaData?;
|
private val _captcha: SourceCaptchaData?;
|
||||||
|
|
||||||
|
val clientId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
var doUpdateCookies: Boolean = true;
|
var doUpdateCookies: Boolean = true;
|
||||||
var doApplyCookies: Boolean = true;
|
var doApplyCookies: Boolean = true;
|
||||||
var doAllowNewCookies: Boolean = true;
|
var doAllowNewCookies: Boolean = true;
|
||||||
val isLoggedIn: Boolean get() = _auth != null;
|
val isLoggedIn: Boolean get() = _auth != null;
|
||||||
|
|
||||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
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() {
|
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
|
||||||
_jsClient = jsClient;
|
_jsClient = jsClient;
|
||||||
|
@ -28,6 +35,7 @@ class JSHttpClient : ManagedHttpClient {
|
||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
|
|
||||||
_currentCookieMap = hashMapOf();
|
_currentCookieMap = hashMapOf();
|
||||||
|
_otherCookieMap = hashMapOf();
|
||||||
if(!auth?.cookieMap.isNullOrEmpty()) {
|
if(!auth?.cookieMap.isNullOrEmpty()) {
|
||||||
for(domainCookies in auth!!.cookieMap!!)
|
for(domainCookies in auth!!.cookieMap!!)
|
||||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||||
|
@ -49,6 +57,45 @@ class JSHttpClient : ManagedHttpClient {
|
||||||
return newClient;
|
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 {
|
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
||||||
val domain = request.url.host.lowercase();
|
val domain = request.url.host.lowercase();
|
||||||
val auth = _auth;
|
val auth = _auth;
|
||||||
|
@ -101,10 +148,10 @@ class JSHttpClient : ManagedHttpClient {
|
||||||
val defaultCookieDomain =
|
val defaultCookieDomain =
|
||||||
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||||
for (header in resp.headers) {
|
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);
|
val cookie = cookieStringToPair(header.second);
|
||||||
var cookieValue = cookie.second;
|
var cookieValue = cookie.second;
|
||||||
var domainToUse = domain;
|
|
||||||
|
|
||||||
if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
|
if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
|
||||||
val cookieParts = cookie.second.split(";");
|
val cookieParts = cookie.second.split(";");
|
||||||
|
@ -124,17 +171,33 @@ class JSHttpClient : ManagedHttpClient {
|
||||||
domainToUse = if (cookieVariables.containsKey("domain"))
|
domainToUse = if (cookieVariables.containsKey("domain"))
|
||||||
cookieVariables["domain"]!!.lowercase();
|
cookieVariables["domain"]!!.lowercase();
|
||||||
else defaultCookieDomain;
|
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))
|
if ((_auth != null || _currentCookieMap.isNotEmpty())) {
|
||||||
_currentCookieMap[domainToUse]!!;
|
val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
|
||||||
else {
|
_currentCookieMap[domainToUse]!!;
|
||||||
val newMap = hashMapOf<String, String>();
|
else {
|
||||||
_currentCookieMap[domainToUse] = newMap
|
val newMap = hashMapOf<String, String>();
|
||||||
newMap;
|
_currentCookieMap[domainToUse] = newMap
|
||||||
|
newMap;
|
||||||
|
}
|
||||||
|
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
||||||
|
cookieMap[cookie.first] = cookieValue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val cookieMap = if (_otherCookieMap.containsKey(domainToUse))
|
||||||
|
_otherCookieMap[domainToUse]!!;
|
||||||
|
else {
|
||||||
|
val newMap = hashMapOf<String, String>();
|
||||||
|
_otherCookieMap[domainToUse] = newMap
|
||||||
|
newMap;
|
||||||
|
}
|
||||||
|
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
||||||
|
cookieMap[cookie.first] = cookieValue;
|
||||||
}
|
}
|
||||||
if(cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
|
||||||
cookieMap[cookie.first] = cookieValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
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.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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
@ -10,13 +11,14 @@ import com.futo.platformplayer.getOrThrow
|
||||||
interface IJSContent: IPlatformContent {
|
interface IJSContent: IPlatformContent {
|
||||||
|
|
||||||
companion object {
|
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 type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
|
||||||
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
|
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
|
||||||
|
|
||||||
//TODO: Temporary workaround for intercepting details in lists
|
//TODO: Temporary workaround for intercepting details in lists
|
||||||
if(pluginType != null && pluginType.endsWith("Details"))
|
if(pluginType != null && pluginType.endsWith("Details"))
|
||||||
return IJSContentDetails.fromV8(config, obj);
|
return IJSContentDetails.fromV8(plugin, obj);
|
||||||
|
|
||||||
return when(ContentType.fromInt(type)) {
|
return when(ContentType.fromInt(type)) {
|
||||||
ContentType.MEDIA -> JSVideo(config, obj);
|
ContentType.MEDIA -> JSVideo(config, obj);
|
||||||
|
|
|
@ -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.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
interface IJSContentDetails: IPlatformContent {
|
interface IJSContentDetails: IPlatformContent {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContentDetails {
|
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
|
||||||
val type: Int = obj.getOrThrow(config, "contentType", "ContentDetails");
|
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
|
||||||
return when(ContentType.fromInt(type)) {
|
return when(ContentType.fromInt(type)) {
|
||||||
ContentType.MEDIA -> JSVideoDetails(config, obj);
|
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||||
ContentType.POST -> JSPostDetails(config, obj);
|
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
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 {
|
override fun convertResult(obj: V8ValueObject): PlatformAuthorLink {
|
||||||
return PlatformAuthorLink.fromV8(config, obj);
|
return PlatformAuthorLink.fromV8(config, obj);
|
||||||
|
|
|
@ -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.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
@ -60,6 +61,7 @@ class JSComment : IPlatformComment {
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,15 +2,16 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSCommentPager : JSPager<IPlatformComment>, IPager<IPlatformComment> {
|
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 {
|
override fun convertResult(obj: V8ValueObject): IPlatformComment {
|
||||||
return JSComment(config, plugin, obj);
|
return JSComment(config, plugin.getUnderlyingPlugin(), obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,15 +3,16 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
||||||
override val sourceConfig: SourcePluginConfig get() = config;
|
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 {
|
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
||||||
return IJSContent.fromV8(config, obj);
|
return IJSContent.fromV8(plugin, obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPlatformLiveEventPager
|
import com.futo.platformplayer.api.media.structures.IPlatformLiveEventPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
@ -10,7 +11,7 @@ import com.futo.platformplayer.getOrThrow
|
||||||
class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
|
class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
|
||||||
override var nextRequest: Int;
|
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");
|
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Looper
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.BuildConfig
|
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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
@ -12,7 +13,7 @@ import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.warnIfMainThread
|
import com.futo.platformplayer.warnIfMainThread
|
||||||
|
|
||||||
abstract class JSPager<T> : IPager<T> {
|
abstract class JSPager<T> : IPager<T> {
|
||||||
protected val plugin: V8Plugin;
|
protected val plugin: JSClient;
|
||||||
protected val config: SourcePluginConfig;
|
protected val config: SourcePluginConfig;
|
||||||
protected var pager: V8ValueObject;
|
protected var pager: V8ValueObject;
|
||||||
|
|
||||||
|
@ -21,9 +22,9 @@ abstract class JSPager<T> : IPager<T> {
|
||||||
private var _hasMorePages: Boolean = false;
|
private var _hasMorePages: Boolean = false;
|
||||||
//private var _morePagesWasFalse: 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.plugin = plugin;
|
||||||
this.pager = pager;
|
this.pager = pager;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
@ -43,7 +44,7 @@ abstract class JSPager<T> : IPager<T> {
|
||||||
override fun nextPage() {
|
override fun nextPage() {
|
||||||
warnIfMainThread("JSPager.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>());
|
pager.invoke("nextPage", arrayOf<Any>());
|
||||||
};
|
};
|
||||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||||
|
|
|
@ -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.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
@ -13,7 +14,7 @@ import com.futo.platformplayer.models.Playlist
|
||||||
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
|
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
|
||||||
override val contents: IPager<IPlatformVideo>;
|
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"));
|
contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
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.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSPlaylistPager : JSPager<IPlatformPlaylist>, IPager<IPlatformPlaylist> {
|
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 {
|
override fun convertResult(obj: V8ValueObject): IPlatformPlaylist {
|
||||||
return JSPlaylist(config, obj);
|
return JSPlaylist(config, obj);
|
||||||
|
|
|
@ -54,6 +54,6 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||||
|
|
||||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||||
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
|
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,18 +1,81 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class JSRequest : JSRequestModifier.IRequest {
|
class JSRequest : IRequest {
|
||||||
override val url: String;
|
private val _v8Url: String?;
|
||||||
override val headers: Map<String, 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";
|
val contextName = "ModifyRequestResponse";
|
||||||
url = obj.getOrThrow(config, "url", contextName);
|
val config = plugin.config;
|
||||||
headers = obj.getOrThrow(config, "headers", contextName);
|
_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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,28 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrNull
|
import com.futo.platformplayer.getOrNull
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class JSRequestModifier {
|
class JSRequestModifier: IRequestModifier {
|
||||||
|
private val _plugin: JSClient;
|
||||||
private val _config: IV8PluginConfig;
|
private val _config: IV8PluginConfig;
|
||||||
private var _modifier: V8ValueObject;
|
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._modifier = modifier;
|
||||||
this._config = config;
|
this._config = plugin.config;
|
||||||
|
val config = plugin.config;
|
||||||
|
|
||||||
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
|
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
|
||||||
|
|
||||||
|
@ -21,22 +30,19 @@ class JSRequestModifier {
|
||||||
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
|
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) {
|
if (_modifier.isClosed) {
|
||||||
return Request(url, headers);
|
return Request(url, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
|
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
|
||||||
_modifier.invoke("modifyRequest", url, headers);
|
_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;
|
data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
|
||||||
}
|
}
|
|
@ -44,13 +44,14 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||||
override val subtitles: List<ISubtitleSource>;
|
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 contextName = "VideoDetails";
|
||||||
|
val config = plugin.config;
|
||||||
description = _content.getOrThrow(config, "description", contextName);
|
description = _content.getOrThrow(config, "description", contextName);
|
||||||
video = JSVideoSourceDescriptor.fromV8(config, _content.getOrThrow(config, "video", contextName));
|
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
|
||||||
dash = JSSource.fromV8DashNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
|
dash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "dash", contextName));
|
||||||
hls = JSSource.fromV8HLSNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
|
hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "hls", contextName));
|
||||||
live = JSSource.fromV8VideoNullable(config, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
|
live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable<V8ValueObject>(config, "live", contextName));
|
||||||
rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0));
|
rating = IRating.fromV8OrDefault(config, _content.getOrDefault<V8ValueObject>(config, "rating", contextName, null), RatingLikes(0));
|
||||||
|
|
||||||
if(!_content.has("subtitles"))
|
if(!_content.has("subtitles"))
|
||||||
|
@ -105,6 +106,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||||
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
|
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
|
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,12 +2,13 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
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.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSVideoPager : JSPager<IPlatformVideo> {
|
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 {
|
override fun convertResult(obj: V8ValueObject): IPlatformVideo {
|
||||||
return JSVideo(config, obj);
|
return JSVideo(config, obj);
|
||||||
|
|
|
@ -2,7 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IAudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
@ -19,8 +21,9 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
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 contextName = "AudioUrlSource";
|
||||||
|
val config = plugin.config;
|
||||||
|
|
||||||
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
|
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
|
||||||
container = _obj.getOrThrow(config, "container", contextName);
|
container = _obj.getOrThrow(config, "container", contextName);
|
||||||
|
|
|
@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IStreamMetaDataSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
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.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
|
||||||
class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
|
class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
|
||||||
|
@ -22,8 +24,9 @@ class JSAudioUrlRangeSource : JSAudioUrlSource, IStreamMetaDataSource {
|
||||||
&& indexEnd != null)
|
&& indexEnd != null)
|
||||||
StreamMetaData(initStart, initEnd, indexStart, indexEnd) else 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 contextName = "JSAudioUrlRangeSource";
|
||||||
|
val config = plugin.config;
|
||||||
|
|
||||||
itagId = _obj.getOrDefault(config, "itagId", contextName, null);
|
itagId = _obj.getOrDefault(config, "itagId", contextName, null);
|
||||||
initStart = _obj.getOrDefault(config, "initStart", contextName, null);
|
initStart = _obj.getOrDefault(config, "initStart", contextName, null);
|
||||||
|
|
|
@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
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.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrNull
|
import com.futo.platformplayer.getOrNull
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
@ -19,9 +21,9 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource {
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
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 contextName = "DashSource";
|
||||||
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
duration = _obj.getOrThrow(config, "duration", contextName);
|
duration = _obj.getOrThrow(config, "duration", contextName);
|
||||||
|
|
|
@ -4,7 +4,9 @@ import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
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.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrNull
|
import com.futo.platformplayer.getOrNull
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.orNull
|
import com.futo.platformplayer.orNull
|
||||||
|
@ -20,8 +22,9 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
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 contextName = "HLSAudioSource";
|
||||||
|
val config = plugin.config;
|
||||||
|
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
@ -33,7 +36,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
|
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
||||||
fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(config, obj);
|
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IHLSManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
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.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrNull
|
import com.futo.platformplayer.getOrNull
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
@ -19,8 +21,9 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
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 contextName = "HLSSource";
|
||||||
|
val config = plugin.config;
|
||||||
|
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
|
|
@ -4,31 +4,47 @@ import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
import androidx.media3.datasource.HttpDataSource
|
import androidx.media3.datasource.HttpDataSource
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
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.api.media.platforms.js.models.JSRequestModifier
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.orNull
|
import com.futo.platformplayer.orNull
|
||||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||||
|
|
||||||
abstract class JSSource {
|
abstract class JSSource {
|
||||||
|
protected val _plugin: JSClient;
|
||||||
protected val _config: IV8PluginConfig;
|
protected val _config: IV8PluginConfig;
|
||||||
protected val _obj: V8ValueObject;
|
protected val _obj: V8ValueObject;
|
||||||
private val _hasRequestModifier: Boolean;
|
val hasRequestModifier: Boolean;
|
||||||
|
private val _requestModifier: JSRequest?;
|
||||||
|
|
||||||
val type : String;
|
val type : String;
|
||||||
|
|
||||||
constructor(type: String, config: IV8PluginConfig, obj: V8ValueObject) {
|
constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
|
||||||
this._config = config;
|
this._plugin = plugin;
|
||||||
|
this._config = plugin.config;
|
||||||
this._obj = obj;
|
this._obj = obj;
|
||||||
this.type = type;
|
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? {
|
fun getRequestModifier(): IRequestModifier? {
|
||||||
if (!_hasRequestModifier || _obj.isClosed) {
|
if(_requestModifier != null)
|
||||||
|
return AdhocRequestModifier { url, headers ->
|
||||||
|
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasRequestModifier || _obj.isClosed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,16 +56,7 @@ abstract class JSSource {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSRequestModifier(_config, result)
|
return JSRequestModifier(_plugin, result)
|
||||||
}
|
|
||||||
|
|
||||||
fun getHttpDataSourceFactory(): HttpDataSource.Factory {
|
|
||||||
val requestModifier = getRequestModifier();
|
|
||||||
return if (requestModifier != null) {
|
|
||||||
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
|
||||||
} else {
|
|
||||||
DefaultHttpDataSource.Factory();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -60,28 +67,28 @@ abstract class JSSource {
|
||||||
const val TYPE_DASH = "DashSource";
|
const val TYPE_DASH = "DashSource";
|
||||||
const val TYPE_HLS = "HLSSource";
|
const val TYPE_HLS = "HLSSource";
|
||||||
|
|
||||||
fun fromV8VideoNullable(config: IV8PluginConfig, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(config, it as V8ValueObject) };
|
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||||
fun fromV8Video(config: IV8PluginConfig, obj: V8ValueObject) : IVideoSource {
|
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
||||||
val type = obj.getString("plugin_type");
|
val type = obj.getString("plugin_type");
|
||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_VIDEOURL -> JSVideoUrlSource(config, obj);
|
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
||||||
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(config, obj);
|
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
|
||||||
TYPE_HLS -> fromV8HLS(config, obj);
|
TYPE_HLS -> fromV8HLS(plugin, obj);
|
||||||
TYPE_DASH -> fromV8Dash(config, obj);
|
TYPE_DASH -> fromV8Dash(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> throw NotImplementedError("Unknown type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun fromV8DashNullable(config: IV8PluginConfig, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(config, it as V8ValueObject) };
|
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
|
||||||
fun fromV8Dash(config: IV8PluginConfig, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(config, obj);
|
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
|
||||||
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
|
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
||||||
fun fromV8HLS(config: IV8PluginConfig, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(config, obj);
|
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");
|
val type = obj.getString("plugin_type");
|
||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(config, obj);
|
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||||
TYPE_AUDIOURL -> JSAudioUrlSource(config, obj);
|
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(config, obj);
|
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> throw NotImplementedError("Unknown type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
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.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
@ -15,15 +16,16 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
|
||||||
override val videoSources: Array<IVideoSource>;
|
override val videoSources: Array<IVideoSource>;
|
||||||
override val audioSources: Array<IAudioSource>;
|
override val audioSources: Array<IAudioSource>;
|
||||||
|
|
||||||
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
|
constructor(plugin: JSClient, obj: V8ValueObject) {
|
||||||
this._obj = obj;
|
this._obj = obj;
|
||||||
|
val config = plugin.config;
|
||||||
val contextName = "UnMuxVideoSource"
|
val contextName = "UnMuxVideoSource"
|
||||||
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
||||||
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Video(config, it as V8ValueObject) }
|
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
|
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Audio(config, it as V8ValueObject) }
|
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
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.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
|
class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
|
||||||
|
@ -14,12 +16,13 @@ class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
|
||||||
override val isUnMuxed: Boolean;
|
override val isUnMuxed: Boolean;
|
||||||
override val videoSources: Array<IVideoSource>;
|
override val videoSources: Array<IVideoSource>;
|
||||||
|
|
||||||
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
|
constructor(plugin: JSClient, obj: V8ValueObject) {
|
||||||
this._obj = obj;
|
this._obj = obj;
|
||||||
|
val config = plugin.config;
|
||||||
val contextName = "VideoSourceDescriptor";
|
val contextName = "VideoSourceDescriptor";
|
||||||
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
||||||
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Video(config, it as V8ValueObject) }
|
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,11 +31,11 @@ class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
|
||||||
const val TYPE_UNMUXED = "UnMuxVideoSourceDescriptor";
|
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")
|
val type = obj.getString("plugin_type")
|
||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_MUXED -> JSVideoSourceDescriptor(config, obj);
|
TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj);
|
||||||
TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(config, obj);
|
TYPE_UNMUXED -> JSUnMuxVideoSourceDescriptor(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type: ${type}");
|
else -> throw NotImplementedError("Unknown type: ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
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.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrNull
|
import com.futo.platformplayer.getOrNull
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
@ -18,8 +20,9 @@ open class JSVideoUrlSource : IVideoUrlSource, JSSource {
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
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 contextName = "JSVideoUrlSource";
|
||||||
|
val config = plugin.config;
|
||||||
|
|
||||||
width = _obj.getOrThrow(config, "width", contextName);
|
width = _obj.getOrThrow(config, "width", contextName);
|
||||||
height = _obj.getOrThrow(config, "height", contextName);
|
height = _obj.getOrThrow(config, "height", contextName);
|
||||||
|
|
|
@ -3,7 +3,9 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
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.IStreamMetaDataSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
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.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
|
||||||
class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
|
class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
|
||||||
|
@ -21,8 +23,9 @@ class JSVideoUrlRangeSource : JSVideoUrlSource, IStreamMetaDataSource {
|
||||||
&& indexEnd != null)
|
&& indexEnd != null)
|
||||||
StreamMetaData(initStart, initEnd, indexStart, indexEnd) else 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 contextName = "JSVideoUrlRangeSource";
|
||||||
|
val config = plugin.config;
|
||||||
|
|
||||||
itagId = _obj.getOrDefault(config, "itagId", contextName, null);
|
itagId = _obj.getOrDefault(config, "itagId", contextName, null);
|
||||||
initStart = _obj.getOrDefault(config, "initStart", contextName, null);
|
initStart = _obj.getOrDefault(config, "initStart", contextName, null);
|
||||||
|
|
|
@ -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.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.exceptions.DownloadException
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
import com.futo.platformplayer.hasAnySource
|
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.isDownloadable
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
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.states.StatePlatform
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSpeed
|
import com.futo.platformplayer.toHumanBytesSpeed
|
||||||
|
import hasAnySource
|
||||||
|
import isDownloadable
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
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.constructs.Event1
|
||||||
import com.futo.platformplayer.engine.exceptions.NoInternetException
|
import com.futo.platformplayer.engine.exceptions.NoInternetException
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
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.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
import com.futo.platformplayer.warnIfMainThread
|
import com.futo.platformplayer.warnIfMainThread
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class V8Plugin {
|
class V8Plugin {
|
||||||
val config: IV8PluginConfig;
|
val config: IV8PluginConfig;
|
||||||
private val _client: ManagedHttpClient;
|
private val _client: ManagedHttpClient;
|
||||||
private val _clientAuth: ManagedHttpClient;
|
private val _clientAuth: ManagedHttpClient;
|
||||||
|
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
||||||
|
|
||||||
|
|
||||||
val httpClient: ManagedHttpClient get() = _client;
|
val httpClient: ManagedHttpClient get() = _client;
|
||||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
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();
|
private val _runtimeLock = Object();
|
||||||
var _runtime : V8Runtime? = null;
|
var _runtime : V8Runtime? = null;
|
||||||
|
|
|
@ -45,7 +45,12 @@ class PackageHttp: V8Package {
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun newClient(withAuth: Boolean): PackageHttpClient {
|
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
|
@V8Function
|
||||||
fun getDefaultClient(withAuth: Boolean): PackageHttpClient {
|
fun getDefaultClient(withAuth: Boolean): PackageHttpClient {
|
||||||
|
@ -187,10 +192,19 @@ class PackageHttp: V8Package {
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private val _defaultHeaders = mutableMapOf<String, String>();
|
private val _defaultHeaders = mutableMapOf<String, String>();
|
||||||
|
@Transient
|
||||||
|
private val _clientId: String?;
|
||||||
|
|
||||||
|
@V8Property
|
||||||
|
fun clientId(): String? {
|
||||||
|
return _clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor(pack: PackageHttp, baseClient: ManagedHttpClient): super() {
|
constructor(pack: PackageHttp, baseClient: ManagedHttpClient): super() {
|
||||||
_package = pack;
|
_package = pack;
|
||||||
_client = baseClient;
|
_client = baseClient;
|
||||||
|
_clientId = if(_client is JSHttpClient) _client.clientId else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
|
|
|
@ -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(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(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(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 }, {
|
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
|
||||||
val c = it.context ?: return@ButtonDefinition;
|
val c = it.context ?: return@ButtonDefinition;
|
||||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||||
|
|
|
@ -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.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
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(
|
groups.add(
|
||||||
BigButtonGroup(c, context.getString(R.string.management),
|
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) {
|
BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.exceptions.RateLimitException
|
import com.futo.platformplayer.exceptions.RateLimitException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
import com.futo.platformplayer.models.SearchType
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
@ -99,6 +100,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
||||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
|
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) {
|
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()");
|
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||||
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
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);
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
};
|
};
|
||||||
_subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
|
_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) {
|
synchronized(_filterLock) {
|
||||||
_subscriptionBar?.setToggles(
|
_subscriptionBar?.setToggles(
|
||||||
|
@ -288,9 +303,15 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
val nowSoon = OffsetDateTime.now().plusMinutes(5);
|
val nowSoon = OffsetDateTime.now().plusMinutes(5);
|
||||||
|
val filterGroup = _subGroup;
|
||||||
return results.filter {
|
return results.filter {
|
||||||
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
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(it.datetime?.isAfter(nowSoon) == true) {
|
||||||
if(!_filterSettings.allowPlanned)
|
if(!_filterSettings.allowPlanned)
|
||||||
return@filter false;
|
return@filter false;
|
||||||
|
|
|
@ -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.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
|
import getHttpDataSourceFactory
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class VideoHelper {
|
class VideoHelper {
|
||||||
|
|
|
@ -1,14 +1,26 @@
|
||||||
package com.futo.platformplayer.models
|
package com.futo.platformplayer.models
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.PresetImages
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import java.io.File
|
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) {
|
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
||||||
if(bitmap != null) {
|
if(bitmap != null) {
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
|
@ -23,6 +35,9 @@ data class ImageVariable(val url: String? = null, val resId: Int? = null, val bi
|
||||||
.load(url)
|
.load(url)
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
|
} else if(!presetName.isNullOrEmpty()) {
|
||||||
|
val resId = PresetImages.getPresetResIdByName(presetName);
|
||||||
|
imageView.setImageResource(resId);
|
||||||
} else if (fallbackResId != -1) {
|
} else if (fallbackResId != -1) {
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
.load(fallbackResId)
|
.load(fallbackResId)
|
||||||
|
@ -44,6 +59,9 @@ data class ImageVariable(val url: String? = null, val resId: Int? = null, val bi
|
||||||
fun fromBitmap(bitmap: Bitmap): ImageVariable {
|
fun fromBitmap(bitmap: Bitmap): ImageVariable {
|
||||||
return ImageVariable(null, null, bitmap);
|
return ImageVariable(null, null, bitmap);
|
||||||
}
|
}
|
||||||
|
fun fromPresetName(str: String): ImageVariable {
|
||||||
|
return ImageVariable(null, null, null, str);
|
||||||
|
}
|
||||||
fun fromFile(file: File): ImageVariable {
|
fun fromFile(file: File): ImageVariable {
|
||||||
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("+");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -133,6 +133,7 @@ class StateApp {
|
||||||
|
|
||||||
//Files
|
//Files
|
||||||
private var _tempDirectory: File? = null;
|
private var _tempDirectory: File? = null;
|
||||||
|
private var _persistentDirectory: File? = null;
|
||||||
|
|
||||||
|
|
||||||
//AutoRotate
|
//AutoRotate
|
||||||
|
@ -165,6 +166,16 @@ class StateApp {
|
||||||
return File(_tempDirectory, name);
|
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 {
|
fun getCurrentSystemAutoRotate(): Boolean {
|
||||||
_context?.let {
|
_context?.let {
|
||||||
systemAutoRotate = android.provider.Settings.System.getInt(
|
systemAutoRotate = android.provider.Settings.System.getInt(
|
||||||
|
@ -290,6 +301,10 @@ class StateApp {
|
||||||
_tempDirectory?.deleteRecursively();
|
_tempDirectory?.deleteRecursively();
|
||||||
}
|
}
|
||||||
_tempDirectory?.mkdirs();
|
_tempDirectory?.mkdirs();
|
||||||
|
_persistentDirectory = File(context.filesDir, "persist");
|
||||||
|
if(_persistentDirectory?.exists() == false) {
|
||||||
|
_persistentDirectory?.mkdirs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
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() {
|
fun runServer() {
|
||||||
if(_server != null)
|
if(_server != null)
|
||||||
|
|
|
@ -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.SourceCaptchaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
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.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
@ -411,6 +412,16 @@ class StatePlugins {
|
||||||
|
|
||||||
fun setPluginSettings(id: String, map: Map<String, String?>) {
|
fun setPluginSettings(id: String, map: Map<String, String?>) {
|
||||||
val newSettings = HashMap(map);
|
val newSettings = HashMap(map);
|
||||||
|
if(id == StateDeveloper.DEV_ID)
|
||||||
|
{
|
||||||
|
val decConfig = StatePlatform.instance.getDevClient()?.config ?: return;
|
||||||
|
for(setting in decConfig.settings) {
|
||||||
|
if(!newSettings.containsKey(setting.variableOrName) || newSettings[setting.variableOrName] == null)
|
||||||
|
newSettings[setting.variableOrName] = setting.default;
|
||||||
|
}
|
||||||
|
StateDeveloper.instance.setDevClientSettings(newSettings);
|
||||||
|
return;
|
||||||
|
}
|
||||||
val plugin = getPlugin(id);
|
val plugin = getPlugin(id);
|
||||||
|
|
||||||
if(plugin != null) {
|
if(plugin != null) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,9 +46,10 @@ class AnyAdapterView<I, T>(view: RecyclerView, adapter: BaseAnyAdapter<I, T, T>,
|
||||||
where T : AnyAdapter.AnyViewHolder<I>{
|
where T : AnyAdapter.AnyViewHolder<I>{
|
||||||
|
|
||||||
companion object {
|
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> {
|
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);
|
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> {
|
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);
|
return AnyAdapterView(this, AnyAdapter.create(list, onCreate), orientation, reversed);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
package com.futo.platformplayer.views.adapters
|
package com.futo.platformplayer.views.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import java.lang.reflect.Constructor
|
import java.lang.reflect.Constructor
|
||||||
|
@ -47,6 +51,7 @@ open class BaseAnyAdapter<I, T : AnyAdapter.AnyViewHolder<I>, IT : ViewHolder> {
|
||||||
cb(item);
|
cb(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun notifyContentChanged() {
|
fun notifyContentChanged() {
|
||||||
adapter.notifyDataSetChanged();
|
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 class Adapter<I, T : AnyViewHolder<I>> : RecyclerView.Adapter<T> {
|
||||||
private val _parent: AnyAdapter<I, T>;
|
private val _parent: AnyAdapter<I, T>;
|
||||||
|
|
||||||
|
|
||||||
constructor(parentAdapter: AnyAdapter<I, T>) {
|
constructor(parentAdapter: AnyAdapter<I, T>) {
|
||||||
_parent = parentAdapter;
|
_parent = parentAdapter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -18,7 +18,8 @@ class SlideUpMenuTextInput : LinearLayout {
|
||||||
private lateinit var _editText: EditText;
|
private lateinit var _editText: EditText;
|
||||||
private lateinit var _inputMethodManager: InputMethodManager;
|
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) {
|
constructor(context: Context, attrs: AttributeSet? = null): super(context, attrs) {
|
||||||
init();
|
init();
|
||||||
|
|
|
@ -3,37 +3,113 @@ package com.futo.platformplayer.views.subscriptions
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.models.Subscription
|
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.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.AnyAdapterView
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.others.ToggleTagView
|
import com.futo.platformplayer.views.others.ToggleTagView
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionBarViewHolder
|
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 {
|
class SubscriptionBar : LinearLayout {
|
||||||
private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
|
private var _adapterView: AnyAdapterView<Subscription, SubscriptionBarViewHolder>? = null;
|
||||||
|
private var _subGroups: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>
|
||||||
private val _tagsContainer: LinearLayout;
|
private val _tagsContainer: LinearLayout;
|
||||||
|
|
||||||
|
private val _groups: ArrayList<SubscriptionGroup>;
|
||||||
|
private var _group: SubscriptionGroup? = null;
|
||||||
|
|
||||||
val onClickChannel = Event1<SerializedChannel>();
|
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) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
inflate(context, R.layout.view_subscription_bar, this);
|
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) {
|
_adapterView = findViewById<RecyclerView>(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) {
|
||||||
it.onClick.subscribe { c ->
|
it.onClick.subscribe { c ->
|
||||||
onClickChannel.emit(c.channel);
|
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);
|
_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) {
|
fun setToggles(vararg buttons: Toggle) {
|
||||||
_tagsContainer.removeAllViews();
|
_tagsContainer.removeAllViews();
|
||||||
|
|
|
@ -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.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
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.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.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
|
import getHttpDataSourceFactory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -402,22 +404,31 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
|
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
|
||||||
Logger.i(TAG, "Loading VideoSource [Url]");
|
Logger.i(TAG, "Loading VideoSource [Url]");
|
||||||
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
|
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
||||||
.setUserAgent(DEFAULT_USER_AGENT))
|
videoSource.getHttpDataSourceFactory()
|
||||||
|
else
|
||||||
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
|
_lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||||
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
|
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
|
||||||
}
|
}
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
|
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
|
||||||
Logger.i(TAG, "Loading VideoSource [Dash]");
|
Logger.i(TAG, "Loading VideoSource [Dash]");
|
||||||
_lastVideoMediaSource = DashMediaSource.Factory(DefaultHttpDataSource.Factory()
|
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
||||||
.setUserAgent(DEFAULT_USER_AGENT))
|
videoSource.getHttpDataSourceFactory()
|
||||||
|
else
|
||||||
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
|
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
|
||||||
.createMediaSource(MediaItem.fromUri(videoSource.url))
|
.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||||
}
|
}
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
|
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
|
||||||
Logger.i(TAG, "Loading VideoSource [HLS]");
|
Logger.i(TAG, "Loading VideoSource [HLS]");
|
||||||
_lastVideoMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
|
val dataSource = if(videoSource is JSSource && videoSource.hasRequestModifier)
|
||||||
.setUserAgent(DEFAULT_USER_AGENT))
|
videoSource.getHttpDataSourceFactory()
|
||||||
|
else
|
||||||
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
|
_lastVideoMediaSource = HlsMediaSource.Factory(dataSource)
|
||||||
.createMediaSource(MediaItem.fromUri(videoSource.url));
|
.createMediaSource(MediaItem.fromUri(videoSource.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,15 +466,21 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
|
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
|
||||||
Logger.i(TAG, "Loading AudioSource [Url]");
|
Logger.i(TAG, "Loading AudioSource [Url]");
|
||||||
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
|
val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
|
||||||
.setUserAgent(DEFAULT_USER_AGENT))
|
audioSource.getHttpDataSourceFactory()
|
||||||
|
else
|
||||||
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
|
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||||
.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
|
.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
|
||||||
}
|
}
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
|
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
|
||||||
Logger.i(TAG, "Loading AudioSource [HLS]");
|
Logger.i(TAG, "Loading AudioSource [HLS]");
|
||||||
_lastAudioMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
|
val dataSource = if(audioSource is JSSource && audioSource.hasRequestModifier)
|
||||||
.setUserAgent(DEFAULT_USER_AGENT))
|
audioSource.getHttpDataSourceFactory()
|
||||||
|
else
|
||||||
|
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||||
|
_lastAudioMediaSource = HlsMediaSource.Factory(dataSource)
|
||||||
.createMediaSource(MediaItem.fromUri(audioSource.url));
|
.createMediaSource(MediaItem.fromUri(audioSource.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ import android.util.Log;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
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 com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
|
@ -60,7 +62,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
private int readTimeoutMs;
|
private int readTimeoutMs;
|
||||||
private boolean allowCrossProtocolRedirects;
|
private boolean allowCrossProtocolRedirects;
|
||||||
private boolean keepPostFor302Redirects;
|
private boolean keepPostFor302Redirects;
|
||||||
@Nullable private JSRequestModifier requestModifier = null;
|
@Nullable private IRequestModifier requestModifier = null;
|
||||||
|
|
||||||
/** Creates an instance. */
|
/** Creates an instance. */
|
||||||
public Factory() {
|
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
|
* @param requestModifier The request modifier that will be used, or {@code null} to use no request modifier
|
||||||
* @return This factory.
|
* @return This factory.
|
||||||
*/
|
*/
|
||||||
public Factory setRequestModifier(@Nullable JSRequestModifier requestModifier) {
|
public Factory setRequestModifier(@Nullable IRequestModifier requestModifier) {
|
||||||
this.requestModifier = requestModifier;
|
this.requestModifier = requestModifier;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -228,7 +230,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
private int responseCode;
|
private int responseCode;
|
||||||
private long bytesToRead;
|
private long bytesToRead;
|
||||||
private long bytesRead;
|
private long bytesRead;
|
||||||
@Nullable private JSRequestModifier requestModifier;
|
@Nullable private IRequestModifier requestModifier;
|
||||||
|
|
||||||
private JSHttpDataSource(
|
private JSHttpDataSource(
|
||||||
@Nullable String userAgent,
|
@Nullable String userAgent,
|
||||||
|
@ -238,7 +240,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
@Nullable RequestProperties defaultRequestProperties,
|
@Nullable RequestProperties defaultRequestProperties,
|
||||||
@Nullable Predicate<String> contentTypePredicate,
|
@Nullable Predicate<String> contentTypePredicate,
|
||||||
boolean keepPostFor302Redirects,
|
boolean keepPostFor302Redirects,
|
||||||
@Nullable JSRequestModifier requestModifier) {
|
@Nullable IRequestModifier requestModifier) {
|
||||||
super(/* isNetwork= */ true);
|
super(/* isNetwork= */ true);
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.connectTimeoutMillis = connectTimeoutMillis;
|
this.connectTimeoutMillis = connectTimeoutMillis;
|
||||||
|
@ -574,8 +576,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
|
|
||||||
String requestUrl = url.toString();
|
String requestUrl = url.toString();
|
||||||
if (requestModifier != null) {
|
if (requestModifier != null) {
|
||||||
JSRequestModifier.IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders);
|
IRequest result = requestModifier.modifyRequest(requestUrl, requestHeaders);
|
||||||
requestUrl = result.getUrl();
|
String modifiedUrl = result.getUrl();
|
||||||
|
requestUrl = (modifiedUrl != null) ? modifiedUrl : requestUrl;
|
||||||
requestHeaders = result.getHeaders();
|
requestHeaders = result.getHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
9
app/src/main/res/drawable/ic_gallery.xml
Normal file
9
app/src/main/res/drawable/ic_gallery.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M164.62,720Q137.96,720 118.98,701.02Q100,682.04 100,655.39L100,304.61Q100,277.96 118.98,258.98Q137.96,240 164.62,240L515.39,240Q542.04,240 561.02,258.98Q580,277.96 580,304.61L580,655.39Q580,682.04 561.02,701.02Q542.04,720 515.39,720L164.62,720ZM692.69,440Q678.39,440 669.19,430.81Q660,421.61 660,407.31L660,272.69Q660,258.38 669.19,249.19Q678.39,240 692.69,240L827.31,240Q841.62,240 850.81,249.19Q860,258.38 860,272.69L860,407.31Q860,421.61 850.81,430.81Q841.62,440 827.31,440L692.69,440ZM700,400L820,400L820,280L700,280L700,400ZM164.62,680L515.39,680Q526.15,680 533.08,673.08Q540,666.15 540,655.39L540,304.61Q540,293.85 533.08,286.92Q526.15,280 515.39,280L164.62,280Q153.85,280 146.92,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 146.92,673.08Q153.85,680 164.62,680ZM187.69,596.15L492.31,596.15L395,466.15L320,566.15L265,493.15L187.69,596.15ZM692.69,720Q678.39,720 669.19,710.81Q660,701.62 660,687.31L660,552.69Q660,538.39 669.19,529.19Q678.39,520 692.69,520L827.31,520Q841.62,520 850.81,529.19Q860,538.39 860,552.69L860,687.31Q860,701.62 850.81,710.81Q841.62,720 827.31,720L692.69,720ZM700,680L820,680L820,560L700,560L700,680ZM140,680Q140,680 140,673.08Q140,666.15 140,655.39L140,304.61Q140,293.85 140,286.92Q140,280 140,280L140,280Q140,280 140,286.92Q140,293.85 140,304.61L140,655.39Q140,666.15 140,673.08Q140,680 140,680L140,680ZM700,400L700,280L700,280L700,400L700,400ZM700,680L700,560L700,560L700,680L700,680Z"/>
|
||||||
|
</vector>
|
BIN
app/src/main/res/drawable/xp_book.jpg
Normal file
BIN
app/src/main/res/drawable/xp_book.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
BIN
app/src/main/res/drawable/xp_code.jpg
Normal file
BIN
app/src/main/res/drawable/xp_code.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 544 KiB |
BIN
app/src/main/res/drawable/xp_controller.jpg
Normal file
BIN
app/src/main/res/drawable/xp_controller.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 986 KiB |
BIN
app/src/main/res/drawable/xp_forest.jpg
Normal file
BIN
app/src/main/res/drawable/xp_forest.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 MiB |
BIN
app/src/main/res/drawable/xp_laptop.jpg
Normal file
BIN
app/src/main/res/drawable/xp_laptop.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 637 KiB |
235
app/src/main/res/layout/fragment_subscriptions_group.xml
Normal file
235
app/src/main/res/layout/fragment_subscriptions_group.xml
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="150dp"
|
||||||
|
android:background="#AAAAAA">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/group_image_background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:src="@drawable/xp_book"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#AA000000">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_delete"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginRight="0dp"
|
||||||
|
android:src="@drawable/ic_trash"
|
||||||
|
app:tint="#CC0000"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
android:visibility="visible"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_settings"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginRight="5dp"
|
||||||
|
android:src="@drawable/ic_settings"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
android:visibility="visible"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/image_group"
|
||||||
|
android:layout_width="110dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
app:circularflow_defaultRadius="10dp"
|
||||||
|
android:layout_marginLeft="30dp"
|
||||||
|
android:src="@drawable/xp_book"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_edit_image"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/image_group"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image_group"
|
||||||
|
android:layout_marginLeft="-15dp"
|
||||||
|
android:layout_marginBottom="-15dp"
|
||||||
|
android:background="@drawable/background_pill">
|
||||||
|
<ImageButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
android:src="@drawable/ic_edit"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image_group"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/image_group"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/image_group"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
android:layout_marginStart="25dp"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_group_title_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_group_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_bold"
|
||||||
|
android:textSize="15dp"
|
||||||
|
android:text="News" />
|
||||||
|
<ImageButton
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginBottom="-5dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
android:src="@drawable/ic_edit"/>
|
||||||
|
</LinearLayout>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_group_meta"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:text="42 creators" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.SearchView
|
||||||
|
android:id="@+id/search_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="0dp"
|
||||||
|
android:paddingEnd="0dp">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_enabled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginBottom="10dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16dp"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:text="@string/enabled" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
android:textColor="@color/gray_ac"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
android:text="@string/these_creators_in_group" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_creators_enabled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginRight="5dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_disabled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginTop="10dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16dp"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:text="@string/disabled" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
android:textColor="@color/gray_ac"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
android:text="@string/these_creators_not_in_group" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_creators_disabled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -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>
|
99
app/src/main/res/layout/list_subscription_group.xml
Normal file
99
app/src/main/res/layout/list_subscription_group.xml
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:id="@+id/root">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/thumb"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="12dp"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:src="@drawable/ic_dragdrop_white"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="75dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/thumb"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/xp_book" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintLeft_toRightOf="@id/image"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/buttons"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sub_group"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginRight="5dp"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textAlignment="textStart"
|
||||||
|
android:text="News" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sub_group_meta"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginRight="5dp"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="10dp"
|
||||||
|
android:textColor="@color/gray_ac"
|
||||||
|
android:textAlignment="textStart"
|
||||||
|
android:text="News" />
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/buttons"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_trash"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
app:tint="@color/pastel_red"
|
||||||
|
android:src="@drawable/ic_trash"
|
||||||
|
/>
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_settings"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
android:src="@drawable/ic_settings"
|
||||||
|
android:visibility="gone"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
134
app/src/main/res/layout/overlay_image_variable.xml
Normal file
134
app/src/main/res/layout/overlay_image_variable.xml
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:background="@color/black"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||||
|
android:id="@+id/topbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
app:title="Select an Image"
|
||||||
|
app:metadata=""
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/container_select"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/gallery_selected_container"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/gallery_selected"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
</LinearLayout>
|
||||||
|
<com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
android:id="@+id/button_gallery"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
app:buttonIcon="@drawable/ic_gallery"
|
||||||
|
app:buttonText="Open Photo Gallery"
|
||||||
|
app:buttonSubText="Pick an image from the gallery" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textSize="20dp"
|
||||||
|
android:text="Preset" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:textColor="@color/gray_ac"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:text="Pick a preset image" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_presets"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
</androidx.recyclerview.widget.RecyclerView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textSize="20dp"
|
||||||
|
android:text="Creator" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:textColor="@color/gray_ac"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:text="Pick a creator as group image" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_creators"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
</androidx.recyclerview.widget.RecyclerView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/container_select"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_select"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
android:text="Select" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
32
app/src/main/res/layout/view_search_bar.xml
Normal file
32
app/src/main/res/layout/view_search_bar.xml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:id="@+id/root">
|
||||||
|
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_search"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:hint="Search"
|
||||||
|
android:paddingEnd="46dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_clear_search"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingStart="18dp"
|
||||||
|
android:paddingEnd="18dp"
|
||||||
|
android:layout_gravity="right|center_vertical"
|
||||||
|
android:visibility="invisible"
|
||||||
|
android:src="@drawable/ic_clear_16dp" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -23,4 +23,9 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal" />
|
android:orientation="horizontal" />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_subgroups"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
36
app/src/main/res/layout/view_subscription_group_bar.xml
Normal file
36
app/src/main/res/layout/view_subscription_group_bar.xml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="78dp"
|
||||||
|
android:layout_height="54dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:id="@+id/root">
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/xp_book" />
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#99000000"
|
||||||
|
android:gravity="center">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sub_group"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:layout_marginRight="5dp"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="News" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -2,7 +2,7 @@
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="27dp"
|
android:layout_height="32dp"
|
||||||
android:paddingStart="15dp"
|
android:paddingStart="15dp"
|
||||||
android:paddingEnd="15dp"
|
android:paddingEnd="15dp"
|
||||||
android:background="@drawable/background_pill"
|
android:background="@drawable/background_pill"
|
||||||
|
|
|
@ -69,6 +69,8 @@
|
||||||
<string name="discover">Discover</string>
|
<string name="discover">Discover</string>
|
||||||
<string name="find_new_video_sources_to_add">Find new video sources to add</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_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="disabled">Disabled</string>
|
||||||
<string name="watch_later">Watch Later</string>
|
<string name="watch_later">Watch Later</string>
|
||||||
<string name="create">Create</string>
|
<string name="create">Create</string>
|
||||||
|
@ -346,6 +348,9 @@
|
||||||
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
|
<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">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="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">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="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>
|
<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="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="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_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="expected_media_content_found">Expected media content, found</string>
|
||||||
<string name="failed_to_load_post">Failed to load post.</string>
|
<string name="failed_to_load_post">Failed to load post.</string>
|
||||||
<string name="replies">replies</string>
|
<string name="replies">replies</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue