Compare commits

..

21 Commits

Author SHA1 Message Date
Kelvin d7f4dd65e8 Stable refs 2023-11-06 14:58:11 +01:00
Kelvin 599b119e62 Remove plugin interaction on main thread for channels 2023-11-06 14:53:24 +01:00
Kelvin 41176464db Fix missing swipe to refresh on tab switch 2023-11-06 14:43:24 +01:00
Kelvin dd0ad19fb9 NewLine subs import, fix no-recent video subscriptions 2023-11-06 14:25:09 +01:00
Kelvin 430625d2fb Fix icon colors 2023-11-06 13:37:18 +01:00
Kelvin 796cd1a776 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-11-06 13:20:42 +01:00
Kelvin baa26af0c0 Only show sub toasts when on subs page, WIP import ui 2023-11-06 13:20:33 +01:00
Koen ea0c27936e Fixed videos not automatically going to next video in playlist when casting. 2023-11-05 15:13:57 +01:00
Kelvin 4aade35d19 Grayjay schema channel support 2023-11-04 18:42:04 +01:00
Kelvin 251a5701af Custom grayjay open video url handling 2023-11-04 18:31:01 +01:00
Kelvin 2da3116111 Fix initial selection of subscription settings 2023-11-03 20:07:08 +01:00
Kelvin 4c82fa1a4a Stable refs 2023-11-03 18:25:40 +01:00
Kelvin 7eef6eece2 Primary claim support, fix sub for clients without type 2023-11-03 18:17:04 +01:00
Kelvin 570f32e980 PlatformUrl support 2023-11-03 15:39:27 +01:00
Kelvin 16a0351125 Per-plugin ratelimit setting 2023-11-03 15:15:18 +01:00
Kelvin 2fa9005806 Keep plugin settings on update 2023-11-03 14:46:43 +01:00
Kelvin 25527997fa Fix channels updating while they shouldnt 2023-11-03 14:37:36 +01:00
Kelvin 4655d8369d Reduce subscription calls, Improve subs sorting, Improve view sorting 2023-11-03 13:34:23 +01:00
Kelvin aeaaace3a4 Subscription settings from creators tab 2023-11-02 23:42:51 +01:00
Kelvin e6997004ff Fix new user crash, show/hide subscription settings button on change, raise import limit to 90 2023-11-02 23:22:42 +01:00
Kelvin 5e1896b7f2 Stable ref 2023-11-02 22:52:29 +01:00
54 changed files with 546 additions and 73 deletions
+20
View File
@@ -92,6 +92,26 @@
<data android:host="*" />
<data android:scheme="file" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter android:autoVerify="true">
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<data android:host="*" />
<data android:scheme="content" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter android:autoVerify="true">
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<data android:host="*" />
<data android:scheme="file" />
<data android:mimeType="application/zip" />
</intent-filter>
<intent-filter android:autoVerify="true">
@@ -603,6 +603,23 @@ class Settings : FragmentedStorageFileJson() {
fun export() {
StateBackup.startExternalBackup();
}
/*
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, 4)
fun import() {
val act = SettingsActivity.getActivity() ?: return;
StateApp.instance.requestFileReadAccess(act, null) {
if(it != null && it.exists()) {
val name = it.name;
val contents = it.readBytes(act);
if(contents != null) {
if(name != null && name.endsWith(".zip", true))
StateBackup.importZipBytes(act, act.lifecycleScope, contents);
}
}
}
}*/
}
@FormField(R.string.payment, FieldForm.GROUP, -1, 14)
@@ -82,6 +82,8 @@ class UISlideOverlays {
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
if(subscription.doNotifications)
menu.selectOption(null, "notifications", true, true);
if(subscription.doFetchLive)
menu.selectOption(null, "fetchLive", true, true);
if(subscription.doFetchStreams)
@@ -497,6 +497,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
};
startActivity(intent);
}
else if(targetData.startsWith("grayjay://video/")) {
val videoUrl = targetData.substring("grayjay://video/".length);
navigate(_fragVideoDetail, videoUrl);
}
else if(targetData.startsWith("grayjay://channel/")) {
val channelUrl = targetData.substring("grayjay://channel/".length);
navigate(_fragMainChannel, channelUrl);
}
}
"content" -> {
if(!handleContent(targetData, intent.type)) {
@@ -583,6 +591,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
StateBackup.importZipBytes(this, lifecycleScope, data);
return true;
}
else if(file.lowercase().endsWith(".txt") || mime == "text/plain") {
return handleUnknownText(String(data));
}
return false;
}
fun handleFile(file: String): Boolean {
@@ -600,6 +611,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
return true;
}
else if(file.lowercase().endsWith(".txt")) {
return handleUnknownText(String(readSharedFile(file)));
}
return false;
}
fun handleReconstruction(recon: String) {
@@ -625,6 +639,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
fun handleUnknownText(text: String): Boolean {
try {
if(text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
navigate(_fragImportSubscriptions, lines);
return true;
}
}
catch(ex: Throwable) {
Logger.e(TAG, ex.message, ex);
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
}
return false;
}
fun handleUnknownJson(name: String?, json: String): Boolean {
val context = this;
@@ -745,6 +773,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
StateSaved.instance.setVideoToOpenBlocking(null);
}
inline fun <reified T> isFragmentActive(): Boolean {
return fragCurrent is T;
}
/**
* Navigate takes a MainFragment, and makes them the current main visible view
@@ -92,6 +92,19 @@ open class JSClient : IPlatformClient {
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
fun getSubscriptionRateLimit(): Int? {
val pluginRateLimit = config.subscriptionRateLimit;
val settingsRateLimit = descriptor.appSettings.rateLimit.getSubRateLimit();
if(settingsRateLimit > 0) {
if(pluginRateLimit != null)
return settingsRateLimit.coerceAtMost(pluginRateLimit);
else
return settingsRateLimit;
}
else
return pluginRateLimit;
}
val onDisabled = Event1<JSClient>();
val onCaptchaException = Event2<JSClient, ScriptCaptchaRequiredException>();
@@ -571,7 +584,7 @@ open class JSClient : IPlatformClient {
if(it.containsKey(claimType)) {
val templates = it[claimType];
if(templates != null)
for(value in values.keys.sortedBy { it }) {
for(value in values.keys.sortedBy { if(it == config.primaryClaimFieldType) Int.MIN_VALUE else it }) {
if(templates.containsKey(value)) {
return templates[value]!!.replace("{{CLAIMVALUE}}", values[value]!!);
}
@@ -41,10 +41,12 @@ class SourcePluginConfig(
val constants: HashMap<String, String> = hashMapOf(),
//TODO: These should be vals...but prob for serialization reasons cannot be changed.
var platformUrl: String? = null,
var subscriptionRateLimit: Int? = null,
var enableInSearch: Boolean = true,
var enableInHome: Boolean = true,
var supportedClaimTypes: List<Int> = listOf()
var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.views.fields.DropdownFieldOptions
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import kotlinx.serialization.Serializable
@@ -79,6 +80,29 @@ class SourcePluginDescriptor {
var enableSearch: Boolean? = null;
}
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
var rateLimit = RateLimit();
@Serializable
class RateLimit {
@FormField(R.string.subscriptions, FieldForm.DROPDOWN, R.string.ratelimit_sub_setting_description, 1)
@DropdownFieldOptions("Plugin defined", "25", "50", "75", "100", "125", "150", "200")
var rateLimitSubs: Int = 0;
fun getSubRateLimit(): Int {
return when(rateLimitSubs) {
0 -> -1
1 -> 25
2 -> 50
3 -> 75
4 -> 100
5 -> 125
6 -> 150
7 -> 200
else -> -1
}
}
}
fun loadDefaults(config: SourcePluginConfig) {
@@ -170,6 +170,10 @@ class ChannelFragment : MainFragment() {
_buttonSubscribe.onSubscribed.subscribe {
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
}
_buttonSubscribe.onUnSubscribed.subscribe {
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
}
_buttonSubscriptionSettings.setOnClickListener {
@@ -382,14 +386,18 @@ class ChannelFragment : MainFragment() {
});
});
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url);
if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
buttons.add(Pair(R.drawable.ic_search) {
_fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO, channel.url));
});
}
_fragment.lifecycleScope.launch(Dispatchers.IO) {
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url);
withContext(Dispatchers.Main) {
if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
buttons.add(Pair(R.drawable.ic_search) {
_fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO, channel.url));
});
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
}
}
}
_buttonSubscribe.setSubscribeChannel(channel);
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
@@ -6,10 +6,12 @@ import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.FrameLayout
import android.widget.Spinner
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
class CreatorsFragment : MainFragment() {
@@ -18,13 +20,16 @@ class CreatorsFragment : MainFragment() {
override val hasBottomBar: Boolean get() = true;
private var _spinnerSortBy: Spinner? = null;
private var _overlayContainer: FrameLayout? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_creators, container, false);
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
_overlayContainer = view.findViewById(R.id.overlay_container);
val spinnerSortBy: Spinner = view.findViewById(R.id.spinner_sortby);
spinnerSortBy.adapter = ArrayAdapter(view.context, R.layout.spinner_item_simple, resources.getStringArray(R.array.subscriptions_sortby_array)).also {
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
@@ -48,6 +53,7 @@ class CreatorsFragment : MainFragment() {
override fun onDestroyMainView() {
super.onDestroyMainView();
_spinnerSortBy = null;
_overlayContainer = null;
}
companion object {
@@ -210,7 +210,7 @@ class ImportSubscriptionsFragment : MainFragment() {
companion object {
val TAG = "ImportSubscriptionsFragment";
private const val MAXIMUM_BATCH_SIZE = 75;
private const val MAXIMUM_BATCH_SIZE = 100;
fun newInstance() = ImportSubscriptionsFragment().apply {}
}
}
@@ -176,9 +176,9 @@ class SubscriptionsFeedFragment : MainFragment() {
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
if(!_bypassRateLimit) {
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true }
Logger.w(TAG, "Refreshing subscriptions with requests:\n" + reqCountStr);
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
if(rateLimitPlugins.any())
throw RateLimitException(rateLimitPlugins.map { it.key.id });
}
@@ -282,6 +282,7 @@ class SubscriptionsFeedFragment : MainFragment() {
loadResults(true);
}
private fun loadCache() {
Logger.i(TAG, "Subscriptions load cache");
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
@@ -301,6 +302,10 @@ class SubscriptionsFeedFragment : MainFragment() {
_taskGetPager.run(withRefetch);
}
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
super.onRestoreCachedData(cachedData);
setTextCentered(if (cachedData.results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
}
private fun loadedResult(pager: IPager<IPlatformContent>) {
Logger.i(TAG, "Subscriptions new pager loaded (${pager.getResults().size})");
@@ -494,8 +494,14 @@ class VideoDetailView : ConstraintLayout {
updatePillButtonVisibilities();
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
if (StateCasting.instance.activeDevice != null) {
val activeDevice = StateCasting.instance.activeDevice;
if (activeDevice != null) {
handlePlayChanged(it);
val v = video;
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
nextVideo();
}
}
};
@@ -610,7 +616,7 @@ class VideoDetailView : ConstraintLayout {
}
val _trackingUpdateTimeLock = Object();
val _trackingUpdateInterval = 3000;
val _trackingUpdateInterval = 2500;
var _trackingLastUpdateTime = System.currentTimeMillis();
var _trackingLastPosition: Long = 0;
var _trackingLastVideo: IPlatformVideoDetails? = null;
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
@@ -53,10 +54,12 @@ class Subscription {
this.channel = channel;
}
fun shouldFetchVideos() = true;
fun shouldFetchStreams() = doFetchStreams && lastLiveStream.getNowDiffDays() < 7;
fun shouldFetchLiveStreams() = doFetchLive && lastLiveStream.getNowDiffDays() < 14;
fun shouldFetchPosts() = doFetchPosts && lastPost.getNowDiffDays() < 2;
fun shouldFetchVideos() = doFetchVideos &&
(lastVideo.getNowDiffDays() < 30 || lastVideoUpdate.getNowDiffDays() >= 1) &&
(lastVideo.getNowDiffDays() < 180 || lastVideoUpdate.getNowDiffDays() >= 3);
fun shouldFetchStreams() = doFetchStreams && (lastLiveStream.getNowDiffDays() < 7);
fun shouldFetchLiveStreams() = doFetchLive && (lastLiveStream.getNowDiffDays() < 14);
fun shouldFetchPosts() = doFetchPosts && (lastPost.getNowDiffDays() < 5);
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
@@ -103,30 +106,39 @@ class Subscription {
else {
interval = 5;
mostRecent = null;
Logger.i("Subscription", "Subscription [${channel.name}]:${type} no results found");
}
when(type) {
ResultCapabilities.TYPE_VIDEOS -> {
uploadInterval = interval;
if(mostRecent != null)
lastVideo = mostRecent;
else if(lastVideo.year > 3000)
lastVideo = OffsetDateTime.MIN;
lastVideoUpdate = OffsetDateTime.now();
}
ResultCapabilities.TYPE_MIXED -> {
uploadInterval = interval;
if(mostRecent != null)
lastVideo = mostRecent;
else if(lastVideo.year > 3000)
lastVideo = OffsetDateTime.MIN;
lastVideoUpdate = OffsetDateTime.now();
}
ResultCapabilities.TYPE_STREAMS -> {
uploadStreamInterval = interval;
if(mostRecent != null)
lastLiveStream = mostRecent;
else if(lastLiveStream.year > 3000)
lastLiveStream = OffsetDateTime.MIN;
lastStreamUpdate = OffsetDateTime.now();
}
ResultCapabilities.TYPE_POSTS -> {
uploadPostInterval = interval;
if(mostRecent != null)
lastPost = mostRecent;
else if(lastPost.year > 3000)
lastPost = OffsetDateTime.MIN;
lastPostUpdate = OffsetDateTime.now();
}
}
@@ -239,6 +239,25 @@ class StateApp {
return state;
}
fun requestFileReadAccess(activity: IWithResultLauncher, path: Uri?, handle: (DocumentFile?)->Unit) {
if(activity is Context) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT);
if(path != null)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.launchForResult(intent, 98) {
if(it.resultCode == Activity.RESULT_OK) {
val uri = it.data?.data;
if(uri != null)
handle(DocumentFile.fromSingleUri(activity, uri));
}
else
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
};
}
}
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit)
{
if(activity is Context)
@@ -452,8 +471,8 @@ class StateApp {
if(Settings.instance.subscriptions.fetchOnAppBoot) {
scope.launch(Dispatchers.IO) {
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true };
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true };
if (isRateLimitReached) {
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
delay(5000);
@@ -407,8 +407,9 @@ class StatePlatform {
return@async searchResult;
} catch(ex: Throwable) {
Logger.e(TAG, "getHomeRefresh", ex);
throw ex;
//throw ex;
//return@async null;
return@async PlaceholderPager(10, { PlatformContentPlaceholder(it.id, ex) });
}
});
}.toList();
@@ -374,7 +374,10 @@ class StatePlugins {
if(icon != null)
iconsDir.saveIconBinary(config.id, icon);
_plugins.save(SourcePluginDescriptor(config, existingAuth?.toEncrypted(), existingCaptcha?.toEncrypted(), flags));
val descriptor = SourcePluginDescriptor(config, existingAuth?.toEncrypted(), existingCaptcha?.toEncrypted(), flags);
descriptor.settings = existing?.settings ?: descriptor.settings;
descriptor.appSettings = existing?.appSettings ?: descriptor.appSettings;
_plugins.save(descriptor);
return null;
}
catch(ex: Throwable) {
@@ -144,14 +144,15 @@ class StatePolycentric {
return DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id });
}
fun getChannelUrls(url: String, channelId: PlatformID? = null): List<String> {
fun getChannelUrls(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false): List<String> {
var polycentricProfile: PolycentricProfile? = null;
try {
polycentricProfile = PolycentricCache.instance.getCachedProfile(url)?.profile;
if (polycentricProfile == null && channelId != null) {
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId) }?.profile;
if(!cacheOnly)
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId) }?.profile;
} else {
Logger.i("StateSubscriptions", "Get polycentric profile cached");
}
@@ -77,7 +77,11 @@ class StateSubscriptions {
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
fun getOldestUpdateTime(): OffsetDateTime {
return getSubscriptions().minOf { it.lastVideoUpdate };
val subs = getSubscriptions();
if(subs.size == 0)
return OffsetDateTime.now();
else
return subs.minOf { it.lastVideoUpdate };
}
fun getGlobalSubscriptionProgress(): Pair<Int, Int> {
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
@@ -237,7 +241,7 @@ class StateSubscriptions {
fun getSubscriptionRequestCount(): Map<JSClient, Int> {
return SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, StateApp.instance.scope)
.countRequests(getSubscriptions());
.countRequests(getSubscriptions().associateWith { StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id, true) });
}
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
@@ -33,7 +33,7 @@ class SmartSubscriptionAlgorithm(
val client = it.value!! as JSClient;
val capabilities = client.getChannelCapabilities();
if(capabilities.hasType(ResultCapabilities.TYPE_MIXED))
if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
return@flatMap listOf(SubscriptionTask(client, sub, it.key, ResultCapabilities.TYPE_MIXED));
else {
val types = listOf(
@@ -42,9 +42,13 @@ class SmartSubscriptionAlgorithm(
if(sub.shouldFetchPosts()) ResultCapabilities.TYPE_POSTS else null,
if(sub.shouldFetchLiveStreams()) ResultCapabilities.TYPE_LIVE else null
).filterNotNull().filter { capabilities.hasType(it) };
return@flatMap types.map {
SubscriptionTask(client, sub, url, it);
};
if(!types.isEmpty())
return@flatMap types.map {
SubscriptionTask(client, sub, url, it);
};
else
listOf(SubscriptionTask(client, sub, url, ResultCapabilities.TYPE_VIDEOS, true))
}
};
};
@@ -59,7 +63,7 @@ class SmartSubscriptionAlgorithm(
for(clientTasks in ordering) {
val limit = clientTasks.first.config.subscriptionRateLimit;
val limit = clientTasks.first.getSubscriptionRateLimit();
if(limit == null || limit <= 0)
finalTasks.addAll(clientTasks.second);
else {
@@ -85,21 +89,21 @@ class SmartSubscriptionAlgorithm(
ResultCapabilities.TYPE_STREAMS -> sub.lastLiveStream;
ResultCapabilities.TYPE_LIVE -> sub.lastLiveStream;
ResultCapabilities.TYPE_POSTS -> sub.lastPost;
else -> sub.lastVideo; //TODO: minimum of all
else -> sub.lastVideo; //TODO: minimum of all?
};
val lastUpdate = when(type) {
ResultCapabilities.TYPE_VIDEOS -> sub.lastVideoUpdate;
ResultCapabilities.TYPE_STREAMS -> sub.lastLiveStreamUpdate;
ResultCapabilities.TYPE_LIVE -> sub.lastLiveStreamUpdate;
ResultCapabilities.TYPE_POSTS -> sub.lastPostUpdate;
else -> sub.lastVideoUpdate; //TODO: minimum of all
else -> sub.lastVideoUpdate; //TODO: minimum of all?
};
val interval = when(type) {
ResultCapabilities.TYPE_VIDEOS -> sub.uploadInterval;
ResultCapabilities.TYPE_STREAMS -> sub.uploadStreamInterval;
ResultCapabilities.TYPE_LIVE -> sub.uploadStreamInterval;
ResultCapabilities.TYPE_POSTS -> sub.uploadPostInterval;
else -> sub.uploadInterval; //TODO: minimum of all
else -> sub.uploadInterval; //TODO: minimum of all?
};
val lastItemDaysAgo = lastItem.getNowDiffHours();
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
@@ -2,6 +2,7 @@ package com.futo.platformplayer.subscription
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
@@ -16,8 +17,10 @@ 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.SubscriptionsFeedFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import kotlinx.coroutines.CoroutineScope
@@ -46,15 +49,16 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val tasksGrouped = tasks.groupBy { it.client }
val taskCount = tasks.filter { !it.fromCache }.size;
val cacheCount = tasks.size - taskCount;
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
" Tasks: ${taskCount}\n" +
" Cached: ${cacheCount}");
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } })" }.joinToString("\n"));
try {
for(clientTasks in tasksGrouped) {
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
val clientCacheCount = clientTasks.value.size - clientTaskCount;
if(clientCacheCount > 0) {
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels. (${clientCacheCount} cached)");
if(clientCacheCount > 0 && clientTaskCount > 0 && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
}
}
@@ -14,6 +14,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
private val _confirmationMessage: String;
var onClick = Event1<Subscription>();
var onSettings = Event1<Subscription>();
var sortBy: Int = 3
set(value) {
field = value;
@@ -33,12 +34,16 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): SubscriptionViewHolder {
val holder = SubscriptionViewHolder(viewGroup);
holder.onClick.subscribe(onClick::emit);
holder.onSettings.subscribe(onSettings::emit);
holder.onTrash.subscribe {
val sub = holder.subscription ?: return@subscribe;
UIDialogs.showConfirmationDialog(_inflater.context, _confirmationMessage, {
StateSubscriptions.instance.removeSubscription(sub.channel.url);
});
};
holder.onSettings.subscribe {
onSettings.emit(it);
};
return holder;
}
@@ -49,10 +54,10 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
private fun updateDataset() {
_sortedDataset = when (sortBy) {
0 -> StateSubscriptions.instance.getSubscriptions().sortedBy({ u -> u.channel.name })
1 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending({ u -> u.channel.name })
2 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackViews }
3 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackViews }
0 -> StateSubscriptions.instance.getSubscriptions().sortedBy({ u -> u.channel.name.lowercase() })
1 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending({ u -> u.channel.name.lowercase() })
2 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackViews * VIEW_PRIORITY + it.playbackSeconds }
3 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackViews * VIEW_PRIORITY + it.playbackSeconds }
4 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackSeconds }
5 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds }
else -> throw IllegalStateException("Invalid sorting algorithm selected.");
@@ -60,4 +65,9 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
notifyDataSetChanged();
}
companion object {
val VIEW_PRIORITY = 36000 * 3;
}
}
@@ -10,6 +10,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.models.Subscription
@@ -18,6 +19,7 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.toHumanBytesSpeed
import com.futo.platformplayer.toHumanTimeIndicator
import com.futo.platformplayer.views.others.CreatorThumbnail
@@ -29,6 +31,7 @@ class SubscriptionViewHolder : ViewHolder {
private val _textName: TextView;
private val _creatorThumbnail: CreatorThumbnail;
private val _buttonTrash: ImageButton;
private val _buttonSettings: ImageButton;
private val _platformIndicator : PlatformIndicator;
private val _textMeta: TextView;
@@ -45,6 +48,7 @@ class SubscriptionViewHolder : ViewHolder {
var onClick = Event1<Subscription>();
var onTrash = Event0();
var onSettings = Event1<Subscription>();
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_subscription, viewGroup, false)) {
_layoutSubscription = itemView.findViewById(R.id.layout_subscription);
@@ -52,6 +56,7 @@ class SubscriptionViewHolder : ViewHolder {
_textMeta = itemView.findViewById(R.id.text_meta);
_creatorThumbnail = itemView.findViewById(R.id.creator_thumbnail);
_buttonTrash = itemView.findViewById(R.id.button_trash);
_buttonSettings = itemView.findViewById(R.id.button_settings);
_platformIndicator = itemView.findViewById(R.id.platform);
_layoutSubscription.setOnClickListener {
@@ -64,6 +69,11 @@ class SubscriptionViewHolder : ViewHolder {
_buttonTrash.setOnClickListener {
onTrash.emit();
};
_buttonSettings.setOnClickListener {
subscription?.let {
onSettings.emit(it);
};
}
}
fun bind(sub: Subscription) {
@@ -21,10 +21,13 @@ class SourceHeaderView : LinearLayout {
private val _sourceDescription: TextView;
private val _sourceVersion: TextView;
private val _sourcePlatformUrl: TextView;
private val _sourceRepositoryUrl: TextView;
private val _sourceScriptUrl: TextView;
private val _sourceSignature: TextView;
private val _sourcePlatformUrlContainer: LinearLayout;
private var _config : SourcePluginConfig? = null;
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
@@ -38,6 +41,8 @@ class SourceHeaderView : LinearLayout {
_sourceVersion = findViewById(R.id.source_version);
_sourceRepositoryUrl = findViewById(R.id.source_repo);
_sourcePlatformUrl = findViewById(R.id.source_platform);
_sourcePlatformUrlContainer = findViewById(R.id.source_platform_container);
_sourceScriptUrl = findViewById(R.id.source_script);
_sourceSignature = findViewById(R.id.source_signature);
@@ -53,6 +58,10 @@ class SourceHeaderView : LinearLayout {
if(!_config?.absoluteScriptUrl.isNullOrEmpty())
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.absoluteScriptUrl)));
};
_sourcePlatformUrl.setOnClickListener {
if(!_config?.platformUrl.isNullOrEmpty())
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.platformUrl)));
};
}
fun loadConfig(config: SourcePluginConfig, script: String?) {
@@ -74,6 +83,12 @@ class SourceHeaderView : LinearLayout {
_sourceRepositoryUrl.text = config.repositoryUrl;
_sourceAuthorID.text = "";
_sourcePlatformUrl.text = config.platformUrl ?: "";
if(!config.platformUrl.isNullOrEmpty())
_sourcePlatformUrlContainer.visibility = VISIBLE;
else
_sourcePlatformUrlContainer.visibility = GONE;
if(!config.authorUrl.isNullOrEmpty())
_sourceBy.setTextColor(resources.getColor(R.color.colorPrimary));
else
@@ -105,5 +120,7 @@ class SourceHeaderView : LinearLayout {
_sourceScriptUrl.text = "";
_sourceRepositoryUrl.text = "";
_sourceAuthorID.text = "";
_sourcePlatformUrl.text = "";
_sourcePlatformUrlContainer.visibility = GONE;
}
}
@@ -36,6 +36,7 @@ class SubscribeButton : LinearLayout {
} else { null };
val onSubscribed = Event1<Subscription>();
val onUnSubscribed = Event1<String>();
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
@@ -82,6 +83,7 @@ class SubscribeButton : LinearLayout {
if (removed != null)
UIDialogs.toast(context, context.getString(R.string.unsubscribed_from) + removed.channel.name);
setIsSubscribed(false);
onUnSubscribed.emit(url);
}
fun setSubscribeChannel(url: String) {
@@ -25,7 +25,7 @@ class SubscriptionBar : LinearLayout {
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.view_subscription_bar, this);
val subscriptions = StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackViews };
val subscriptions = StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds };
_adapterView = findViewById<RecyclerView>(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) {
it.onClick.subscribe { c ->
onClickChannel.emit(c.channel);
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#000000" />
<corners android:radius="4dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
+1 -2
View File
@@ -2,8 +2,7 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M120,800L120,720L600,720L600,800L120,800ZM640,520Q557,520 498.5,461.5Q440,403 440,320Q440,237 498.5,178.5Q557,120 640,120Q723,120 781.5,178.5Q840,237 840,320Q840,403 781.5,461.5Q723,520 640,520ZM120,480L120,400L372,400Q379,422 388,442Q397,462 410,480L120,480ZM120,640L120,560L496,560Q519,574 545,583.5Q571,593 600,597L600,640L120,640ZM620,360L660,360L660,200L620,200L620,360ZM640,440Q648,440 654,434Q660,428 660,420Q660,412 654,406Q648,400 640,400Q632,400 626,406Q620,412 620,420Q620,428 626,434Q632,440 640,440Z"/>
@@ -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="M420,600L540,600L517,471Q537,461 548.5,442Q560,423 560,400Q560,367 536.5,343.5Q513,320 480,320Q447,320 423.5,343.5Q400,367 400,400Q400,423 411.5,442Q423,461 443,471L420,600ZM480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M120,720L120,640L600,640L600,720L120,720ZM120,520L120,440L840,440L840,520L120,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
</vector>
+1 -2
View File
@@ -2,8 +2,7 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M560,600Q577,600 589.5,587.5Q602,575 602,558Q602,541 589.5,528.5Q577,516 560,516Q543,516 530.5,528.5Q518,541 518,558Q518,575 530.5,587.5Q543,600 560,600ZM530,472L590,472Q590,443 596,429.5Q602,416 624,394Q654,364 664,345.5Q674,327 674,302Q674,257 642.5,228.5Q611,200 560,200Q519,200 488.5,223Q458,246 446,284L500,306Q509,281 524.5,268.5Q540,256 560,256Q584,256 599,269.5Q614,283 614,306Q614,320 606,332.5Q598,345 578,364Q545,393 537.5,409.5Q530,426 530,472ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
+1 -2
View File
@@ -2,8 +2,7 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M160,720L800,720Q800,720 800,720Q800,720 800,720L800,400L520,400L520,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L160,720Z"/>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M640,480L640,400L720,400L720,480L640,480ZM640,560L560,560L560,480L640,480L640,560ZM640,640L640,560L720,560L720,640L640,640ZM447,320L367,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L560,720L560,640L640,640L640,720L800,720Q800,720 800,720Q800,720 800,720L800,320Q800,320 800,320Q800,320 800,320L640,320L640,400L560,400L560,320L447,320ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L160,720Q160,720 160,720Q160,720 160,720L160,320Q160,320 160,320Q160,320 160,320L160,320L160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720Z"/>
</vector>
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@color/gray_1d">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="40dp">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/update_spinner"
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_move_up" />
<TextView
android:id="@+id/text_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:layout_gravity="center"
android:textSize="14dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular" />
</FrameLayout>
<TextView
android:id="@+id/text_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/there_is_an_update_available_do_you_wish_to_update"
android:textSize="14dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:layout_marginTop="30dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="10dp"
android:layout_marginTop="28dp"
android:layout_marginBottom="28dp">
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@color/gray_1d">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="40dp">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/update_spinner"
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_move_up" />
<TextView
android:id="@+id/text_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:layout_gravity="center"
android:textSize="14dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular" />
</FrameLayout>
<TextView
android:id="@+id/text_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/import_options"
android:textAlignment="center"
android:textSize="14dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:layout_marginTop="30dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="10dp"
android:layout_marginTop="28dp"
android:layout_marginBottom="28dp">
<com.futo.platformplayer.views.buttons.BigButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_zip"
app:buttonText="Import Grayjay export (.zip)"
android:layout_margin="5dp"
app:buttonBackground="@drawable/background_big_button_black"
app:buttonSubText="Pick a Grayjay export zip file" />
<com.futo.platformplayer.views.buttons.BigButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_encrypted"
android:alpha="0.5"
app:buttonBackground="@drawable/background_big_button_black"
app:buttonText="Import Grayjay Auto-Backup (.ezip)"
android:layout_margin="5dp"
app:buttonSubText="Pick a Grayjay auto-backup encrypted zip file" />
<com.futo.platformplayer.views.buttons.BigButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:buttonIcon="@drawable/ic_lines"
android:alpha="0.5"
app:buttonBackground="@drawable/background_big_button_black"
app:buttonText="Import Line Text file (.txt)"
app:buttonSubText="Pick a text file with one entry per line" />
<com.futo.platformplayer.views.buttons.BigButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:buttonIcon="@drawable/ic_play"
app:buttonBackground="@drawable/background_big_button_black"
app:buttonText="Import NewPipe Subscriptions (.json)"
app:buttonSubText="Pick a NewPipe subscriptions json file" />
<Button
android:id="@+id/button_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/close"
android:layout_marginTop="20dp"
android:textSize="14dp"
android:fontFamily="@font/inter_regular"
android:textColor="@color/colorPrimary"
android:background="@color/transparent" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -55,4 +55,11 @@
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+18 -7
View File
@@ -56,13 +56,24 @@
</LinearLayout>
</LinearLayout>
<ImageButton
android:id="@+id/button_settings"
android:layout_width="50dp"
android:layout_height="40dp"
app:srcCompat="@drawable/ic_settings"
android:scaleType="fitCenter"
android:paddingStart="5dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:paddingEnd="0dp" />
<ImageButton
android:id="@+id/button_trash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_trash_18dp"
android:paddingStart="20dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingEnd="20dp" />
android:layout_width="60dp"
android:layout_height="40dp"
app:srcCompat="@drawable/ic_trash"
android:scaleType="fitCenter"
android:paddingStart="5dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:paddingEnd="5dp" />
</LinearLayout>
@@ -100,6 +100,30 @@
tools:text="3" />
</LinearLayout>
<!--Platform Url-->
<LinearLayout
android:id="@+id/source_platform_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14dp"
android:textColor="@color/white"
android:layout_marginTop="10dp"
android:fontFamily="@font/inter_light"
android:text="@string/platform_url" />
<TextView
android:id="@+id/source_platform"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14dp"
android:textColor="@color/colorPrimary"
android:fontFamily="@font/inter_extra_light"
tools:text="https://some.platform.url" />
</LinearLayout>
<!--Repo Url-->
<LinearLayout
android:layout_width="match_parent"
+9
View File
@@ -28,6 +28,7 @@
<string name="update">Update</string>
<string name="close">Close</string>
<string name="never">Never</string>
<string name="import_options">Select any of the following available import options.</string>
<string name="there_is_an_update_available_do_you_wish_to_update">There is an update available, do you wish to update?</string>
<string name="downloading_update">Downloading update…</string>
<string name="installing_update">Installing update…</string>
@@ -98,6 +99,7 @@
<string name="are_you_sure_delete_historical">Are you sure you want to remove these historical entries?</string>
<string name="removed">removed</string>
<string name="add_source">Add Source</string>
<string name="platform_url">Platform URL</string>
<string name="repository_url">Repository URL</string>
<string name="script_url">Script URL</string>
<string name="source_permissions_explanation">These are the permissions the plugin requires to function</string>
@@ -301,6 +303,8 @@
<string name="enable_casting">Enable casting</string>
<string name="experimental_background_update_for_subscriptions_cache">Experimental background update for subscriptions cache</string>
<string name="export_data">Export Data</string>
<string name="import_data">Import Data</string>
<string name="import_data_description">Select a file to import, support various files (alternative to opening directly)</string>
<string name="external_storage">External Storage</string>
<string name="feed_style">Feed Style</string>
<string name="fetch_on_app_boot">Fetch on app boot</string>
@@ -396,6 +400,10 @@
<string name="various_tests_against_a_custom_source">Various tests against a custom source</string>
<string name="writes_to_disk_till_no_space_is_left">Writes to disk till no space is left</string>
<string name="visibility">Visibility</string>
<string name="ratelimit">Rate-limit</string>
<string name="ratelimit_description">Settings related to rate-limiting this plugin\'s behavior</string>
<string name="ratelimit_sub_setting">Rate-limit Subscriptions</string>
<string name="ratelimit_sub_setting_description">Limit the amount of subscription requests made</string>
<string name="enable_where_this_plugins_content_are_visible">Enable where this plugin\'s content are visible</string>
<string name="show_content_in_home_tab">Show content in home tab</string>
<string name="show_content_in_search_results">Show content in search results</string>
@@ -427,6 +435,7 @@
<string name="unknown_url_format">Unknown url format</string>
<string name="failed_to_handle_file">Failed to handle file</string>
<string name="unknown_reconstruction_type">Unknown reconstruction type</string>
<string name="failed_to_parse_text_file">Failed to parse text file</string>
<string name="failed_to_parse_newpipe_subscriptions">Failed to parse NewPipe Subscriptions</string>
<string name="failed_to_generate_qr_code">Failed to generate QR code</string>
<string name="share_text">Share Text</string>