Compare commits

...

16 Commits

Author SHA1 Message Date
Kelvin ca971a0e77 Fix playlist edit name 2023-10-20 19:13:44 +02:00
Kelvin a45a0f9a8a Fix soundcloud missing whitelist domain 2023-10-20 18:40:13 +02:00
Kelvin c2dce52a5b Fix Twitch live streams on channel, hasMore can now be nullable defaulting to false 2023-10-20 17:49:26 +02:00
Kelvin a2c63c59c5 Hide buy on playstore, margins on captcha button 2023-10-20 17:38:33 +02:00
Kelvin 7e54a2ce3d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-10-20 17:20:43 +02:00
Kelvin 5b7fb2c818 Consent reject now works, app now intercepts redirects 2023-10-20 17:20:36 +02:00
Koen da0ac281e2 Added button to open FAQ from settings. 2023-10-20 14:43:01 +02:00
Koen 576b37f64c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-10-20 14:34:53 +02:00
Koen 26c2db5023 Handle pager getComments returning null silently. 2023-10-20 14:34:41 +02:00
Kelvin f344dbf35c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-10-20 14:34:01 +02:00
Kelvin a04acbd4a5 Home all error fix, multi plugin cookie support, completion url semi-wildcard support, delete captcha button, critical exception support, dev portal can now request captchas. WIP Consent fix 2023-10-20 14:33:37 +02:00
Koen bd48aba8d3 Added text for FeedView which allows users to be informed what to do when sub feed is empty. 2023-10-20 14:14:02 +02:00
Koen 12b73bb248 Maximum import 75 subscriptions at once. 2023-10-20 13:17:25 +02:00
Koen c3ff897ef4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-10-20 12:21:33 +02:00
Koen 242728fbe7 Fix deferred null. 2023-10-20 12:20:56 +02:00
Kelvin 14df7c8d43 Missing negative sub hide, youtube null exception catch, autobackup password field type fix 2023-10-20 00:27:25 +02:00
34 changed files with 315 additions and 91 deletions
@@ -217,6 +217,9 @@ function pluginUpdateTestPlugin(config) {
}
function pluginLoginTestPlugin() {
return syncGET("/plugin/loginTestPlugin", {});
}//captchaLoginTestPlugin
function pluginCaptchaTestPlugin(url, html) {
return syncPOST("/plugin/captchaTestPlugin?url=" + url, {}, html);
}
function pluginLogoutTestPlugin() {
return syncGET("/plugin/logoutTestPlugin", {});
+9
View File
@@ -681,6 +681,9 @@
});
}, 1000);
},
captchaTestPlugin() {
captchaLoginTestPlugin();
},
logoutTestPlugin() {
pluginLogoutTestPlugin();
},
@@ -838,6 +841,12 @@
this.Testing.lastResultError = "";
}
catch(ex) {
if(ex.plugin_type == "CaptchaRequiredException") {
let shouldCaptcha = confirm("Do you want to request captcha?");
if(shouldCaptcha) {
pluginCaptchaTestPlugin(ex.url, ex.body);
}
}
console.error("Failed to run test for " + req.title, ex);
this.Testing.lastResult = ""
if(ex.message)
+5
View File
@@ -72,6 +72,11 @@ class CaptchaRequiredException extends Error {
this.body = body;
}
}
class CriticalException extends ScriptException {
constructor(msg) {
super("CriticalException", msg);
}
}
class UnavailableException extends ScriptException {
constructor(msg) {
super("UnavailableException", msg);
@@ -6,6 +6,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.webkit.CookieManager
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.*
import com.futo.platformplayer.api.http.ManagedHttpClient
@@ -44,7 +45,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(
"Manage Polycentric identity", FieldForm.BUTTON,
"Manage your Polycentric identity", -2
"Manage your Polycentric identity", -3
)
fun managePolycentricIdentity() {
SettingsActivity.getActivity()?.let {
@@ -56,6 +57,19 @@ class Settings : FragmentedStorageFileJson() {
}
}
@FormField(
"Open FAQ", FieldForm.BUTTON,
"Get answers to common questions", -2
)
fun openFAQ() {
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://grayjay.app/faq.html"))
SettingsActivity.getActivity()?.startActivity(browserIntent);
} catch (e: Throwable) {
//Ignored
}
}
@FormField(
"Submit feedback", FieldForm.BUTTON,
"Give feedback on the application", -1
@@ -6,6 +6,7 @@ import com.futo.platformplayer.logging.Logger
import okhttp3.Call
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
@@ -28,7 +29,11 @@ open class ManagedHttpClient {
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder;
client = builder.build();
client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request));
return@addNetworkInterceptor response;
}.build();
}
open fun clone(): ManagedHttpClient {
@@ -116,7 +121,7 @@ open class ManagedHttpClient {
fun execute(request : Request) : Response {
ensureNotMainThread();
beforeRequest(request);
//beforeRequest(request);
Logger.v(TAG, "HTTP Request [${request.method}] ${request.url} - [${if(request.body != null) request.body.size else 0}]");
@@ -156,23 +161,16 @@ open class ManagedHttpClient {
if(true)
Logger.v(TAG, "HTTP Response [${request.method}] ${request.url} - [${time}ms]");
afterRequest(request, resp);
//afterRequest(request, resp);
return resp;
}
//Set Listeners
fun setOnBeforeRequest(listener : (Request)->Unit) {
this.onBeforeRequest = listener;
open fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
return request;
}
fun setOnAfterRequest(listener : (Request, Response)->Unit) {
this.onAfterRequest = listener;
}
open fun beforeRequest(request: Request) {
onBeforeRequest?.invoke(request);
}
open fun afterRequest(request: Request, resp: Response) {
onAfterRequest?.invoke(request, resp);
open fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
return resp;
}
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.states.StateApp
import java.util.*
class DevJSClient : JSClient {
@@ -24,6 +25,10 @@ class DevJSClient : JSClient {
_auth = auth;
_captcha = captcha;
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
onCaptchaException.subscribe { client, captcha ->
StateApp.instance.handleCaptchaException(client, captcha);
}
}
//TODO: Misisng auth/captcha pass on purpose?
constructor(context: Context, descriptor: SourcePluginDescriptor, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, savedState: String? = null, devID: String? = null): super(context, descriptor, savedState, script) {
@@ -31,6 +36,10 @@ class DevJSClient : JSClient {
_auth = auth;
_captcha = captcha;
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
onCaptchaException.subscribe { client, captcha ->
StateApp.instance.handleCaptchaException(client, captcha);
}
}
fun setCaptcha(captcha: SourceCaptchaData? = null) {
@@ -4,6 +4,7 @@ import android.content.Context
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueNull
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
@@ -23,6 +24,7 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.internal.*
import com.futo.platformplayer.api.media.platforms.js.models.*
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
@@ -431,8 +433,11 @@ open class JSClient : IPlatformClient {
@JSDocsParameter("url", "A content url (this platform)")
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
ensureEnabled();
return@isBusyWith JSCommentPager(config, plugin,
plugin.executeTyped("source.getComments(${Json.encodeToString(url)})"));
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
return@isBusyWith EmptyPager<IPlatformComment>();
}
return@isBusyWith JSCommentPager(config, plugin, pager);
}
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
@JSDocsParameter("comment", "Comment object that was returned by getComments")
@@ -31,8 +31,12 @@ class JSHttpClient : ManagedHttpClient {
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
if(!captcha?.cookieMap.isNullOrEmpty()) {
for(domainCookies in captcha!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
for(domainCookies in captcha!!.cookieMap!!) {
if(_currentCookieMap.containsKey(domainCookies.key))
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
else
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
}
}
@@ -46,14 +50,18 @@ class JSHttpClient : ManagedHttpClient {
return newClient;
}
override fun beforeRequest(request: Request) {
val domain = Uri.parse(request.url).host!!.lowercase();
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
val domain = request.url.host.lowercase();
val auth = _auth;
val newBuilder = if(auth != null || doApplyCookies)
request.newBuilder();
else
null;
if (auth != null) {
//TODO: Possibly add doApplyHeaders
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
request.headers[header.key] = header.value;
newBuilder?.header(header.key, header.value);
}
if(doApplyCookies) {
@@ -68,34 +76,37 @@ class JSHttpClient : ManagedHttpClient {
if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
request.headers["Cookie"] = cookieString;
val existingCookies = request.headers["Cookie"];
if(!existingCookies.isNullOrEmpty())
newBuilder?.header("Cookie", existingCookies.trim(';') + "; " + cookieString);
else
newBuilder?.header("Cookie", cookieString);
}
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
}
}
_jsClient?.validateUrlOrThrow(request.url);
super.beforeRequest(request)
_jsClient?.validateUrlOrThrow(request.url.toString());
return newBuilder?.let { it.build() } ?: request;
}
override fun afterRequest(request: Request, resp: Response) {
super.afterRequest(request, resp)
override fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
if(doUpdateCookies) {
val domain = Uri.parse(request.url).host!!.lowercase();
val domainParts = domain!!.split(".");
val domain = resp.request.url.host.lowercase();
val domainParts = domain.split(".");
val defaultCookieDomain =
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
for (header in resp.headers) {
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.key.lowercase() == "set-cookie") {
val newCookies = cookieStringToMap(header.value);
for (cookie in newCookies) {
val endIndex = cookie.value.indexOf(";");
var cookieValue = cookie.value;
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") {
//val newCookies = cookieStringToMap(header.second.split("; "));
val cookie = cookieStringToPair(header.second);
//for (cookie in newCookies) {
var cookieValue = cookie.second;
var domainToUse = domain;
if (endIndex > 0) {
val cookieParts = cookie.value.split(";");
if (!cookie.first.isNullOrEmpty() && !cookie.second.isNullOrEmpty()) {
val cookieParts = cookie.second.split(";");
if (cookieParts.size == 0)
continue;
cookieValue = cookieParts[0].trim();
@@ -121,24 +132,29 @@ class JSHttpClient : ManagedHttpClient {
_currentCookieMap!!.put(domainToUse, newMap)
newMap;
}
if(cookieMap.containsKey(cookie.key) || doAllowNewCookies)
cookieMap.put(cookie.key, cookieValue);
}
if(cookieMap.containsKey(cookie.first) || doAllowNewCookies)
cookieMap.put(cookie.first, cookieValue);
//}
}
}
}
return resp;
}
private fun cookieStringToMap(parts: List<String>): Map<String, String> {
val map = hashMapOf<String, String>();
for(cookie in parts) {
val cookieKey = cookie.substring(0, cookie.indexOf("="));
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
map.put(cookieKey.trim(), cookieVal.trim());
val pair = cookieStringToPair(cookie)
map.put(pair.first, pair.second);
}
return map;
}
private fun cookieStringToPair(cookie: String): Pair<String, String> {
val cookieKey = cookie.substring(0, cookie.indexOf("="));
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
return Pair(cookieKey.trim(), cookieVal.trim());
}
//Prints out code for test reproduction..
fun printTestCode(url: String, body: ByteArray?, headers: Map<String, String>, cookieString: String, allHeaders: Map<String, String>? = null) {
@@ -7,6 +7,7 @@ import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.warnIfMainThread
@@ -27,7 +28,7 @@ abstract class JSPager<T> : IPager<T> {
this.pager = pager;
this.config = config;
_hasMorePages = pager.getOrThrow(config, "hasMore", "Pager");
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
getResults();
}
@@ -45,7 +46,7 @@ abstract class JSPager<T> : IPager<T> {
pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pager.invoke("nextPage", arrayOf<Any>());
};
_hasMorePages = pager.getOrThrow(config, "hasMore", "Pager");
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
/*
try {
@@ -1,6 +1,7 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueNull
import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
@@ -99,8 +100,11 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
return getCommentsJS(client);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
return null;
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
}
}
@@ -69,9 +69,17 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
if(pagerToAdd == null) {
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
_currentPager = PlaceholderPager(5) {
_pagersReusable.add((PlaceholderPager(5) {
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
} as IPager<T>;
} as IPager<T>).asReusable());
_currentPager = recreatePager(getCurrentSubPagers());
if(_currentPager is MultiParallelPager<*>)
runBlocking { (_currentPager as MultiParallelPager).initialize(); };
else if(_currentPager is MultiPager<*>)
(_currentPager as MultiPager).initialize()
onPagerChanged.emit(_currentPager);
}
return;
@@ -1,6 +1,7 @@
package com.futo.platformplayer.developer
import android.content.Context
import com.futo.platformplayer.activities.CaptchaActivity
import com.futo.platformplayer.activities.LoginActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.HttpContext
@@ -201,6 +202,28 @@ class DeveloperEndpoints(private val context: Context) {
context.respondCode(500, (ex::class.simpleName + ":" + ex.message) ?: "", "text/plain")
}
}
@HttpPOST("/plugin/captchaTestPlugin")
fun pluginCaptchaTestPlugin(context: HttpContext) {
val config = _testPlugin?.config as SourcePluginConfig;
val url = context.query.get("url")
val html = context.readContentString();
try {
val captchaConfig = config.captcha;
if(captchaConfig == null) {
context.respondCode(403, "This plugin doesn't support captcha");
return;
}
CaptchaActivity.showCaptcha(StateApp.instance.context, config, url, html) {
_testPluginVariables.clear();
_testPlugin = V8Plugin(StateApp.instance.context, config, null, JSHttpClient(null, null, it), JSHttpClient(null, null, it));
};
context.respondCode(200, "Captcha started");
}
catch(ex: Throwable) {
context.respondCode(500, (ex::class.simpleName + ":" + ex.message) ?: "", "text/plain")
}
}
@HttpGET("/plugin/loginTestPlugin")
fun pluginLoginTestPlugin(context: HttpContext) {
val config = _testPlugin?.config as SourcePluginConfig;
@@ -298,6 +298,7 @@ class V8Plugin {
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
when(pluginType) {
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
"CriticalException" -> throw ScriptCriticalException(config, msg, innerEx, stack, code);
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
@@ -0,0 +1,17 @@
package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow
open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException"));
}
}
}
@@ -10,6 +10,7 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import com.futo.futopay.PaymentConfigurations
import com.futo.futopay.PaymentManager
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
@@ -68,9 +69,12 @@ class BuyFragment : MainFragment() {
}
}
_buttonBuy.setOnClickListener {
buy();
}
if(!BuildConfig.IS_PLAYSTORE_BUILD)
_buttonBuy.setOnClickListener {
buy();
}
else
_buttonBuy.visibility = View.GONE;
_buttonPaid.setOnClickListener {
paid();
}
@@ -39,6 +39,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private val _spinnerSortBy: Spinner;
private val _containerSortBy: LinearLayout;
private val _tagsView: TagsView;
private val _textCentered: TextView;
protected val _toolbarContentView: LinearLayout;
@@ -68,6 +69,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
this.fragment = fragment;
inflater.inflate(R.layout.fragment_feed, this);
_textCentered = findViewById(R.id.text_centered);
_progress_bar = findViewById(R.id.progress_bar);
_progress_bar.inactiveColor = Color.TRANSPARENT;
@@ -169,6 +171,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_recyclerResults.addOnScrollListener(_scrollListener);
}
protected fun setTextCentered(text: String?) {
_textCentered.text = text;
}
fun onResume() {
//Reload the pager if the plugin was killed
val pager = recyclerData.pager;
@@ -69,6 +69,8 @@ class ImportSubscriptionsFragment : MainFragment() {
private var _currentLoadIndex = 0;
private var _taskLoadChannel: TaskHandler<String, IPlatformChannel>;
private var _counter: Int = 0;
private var _limitToastShown = false;
constructor(fragment: ImportSubscriptionsFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
@@ -104,6 +106,7 @@ class ImportSubscriptionsFragment : MainFragment() {
setLoading(false);
_taskLoadChannel = TaskHandler<String, IPlatformChannel>({_fragment.lifecycleScope}, { link ->
_counter++;
val channel: IPlatformChannel = StatePlatform.instance.getChannelLive(link, false);
return@TaskHandler channel;
}).success {
@@ -124,6 +127,8 @@ class ImportSubscriptionsFragment : MainFragment() {
}
fun onShown(parameter: Any ?, isBack: Boolean) {
_counter = 0;
_limitToastShown = false;
updateSelected();
val itemsRemoved = _items.size;
@@ -157,6 +162,15 @@ class ImportSubscriptionsFragment : MainFragment() {
private fun load() {
setLoading(true);
if (_counter >= MAXIMUM_BATCH_SIZE) {
if (!_limitToastShown) {
_limitToastShown = true;
UIDialogs.toast(context, "Stopped after $MAXIMUM_BATCH_SIZE to avoid rate limit, re-enter to import rest");
}
setLoading(false);
return;
}
_taskLoadChannel.run(_links[_currentLoadIndex]);
}
@@ -196,6 +210,7 @@ class ImportSubscriptionsFragment : MainFragment() {
companion object {
val TAG = "ImportSubscriptionsFragment";
private const val MAXIMUM_BATCH_SIZE = 75;
fun newInstance() = ImportSubscriptionsFragment().apply {}
}
}
@@ -601,7 +601,7 @@ class PostDetailFragment : MainFragment {
val subscribers = value?.author?.subscribers;
if(subscribers != null && subscribers > 0) {
_channelMeta.visibility = View.VISIBLE;
_channelMeta.text = value.author.subscribers!!.toHumanNumber() + " subscribers";
_channelMeta.text = if((value.author?.subscribers ?: 0) > 0) value.author.subscribers!!.toHumanNumber() + " subscribers" else "";
} else {
_channelMeta.visibility = View.GONE;
_channelMeta.text = "";
@@ -258,11 +258,25 @@ class SourceDetailFragment : MainFragment() {
}
}
val clientIfExists = StatePlugins.instance.getPlugin(config.id);
groups.add(
BigButtonGroup(c, "Management",
BigButton(c, "Uninstall", "Removes the plugin from the app", R.drawable.ic_block) {
uninstallSource();
}.withBackground(R.drawable.background_big_button_red)
}.withBackground(R.drawable.background_big_button_red).apply {
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
};
},
if(clientIfExists?.captchaEncrypted != null)
BigButton(c, "Delete Captcha", "Deletes stored captcha answer for this plugin", R.drawable.ic_block) {
clientIfExists?.updateCaptcha(null);
}.apply {
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
};
}.withBackground(R.drawable.background_big_button_red)
else null
)
)
@@ -13,6 +13,7 @@ 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.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.cache.ChannelContentCache
import com.futo.platformplayer.constructs.TaskHandler
@@ -280,8 +281,12 @@ class SubscriptionsFeedFragment : MainFragment() {
Logger.i(TAG, "Subscriptions load");
if(recyclerData.results.size == 0) {
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
Logger.i(TAG, "Subscription show cache (${cachePager.getResults().size})");
val results = cachePager.getResults();
Logger.i(TAG, "Subscription show cache (${results.size})");
setTextCentered(if (results.isEmpty()) "No results found\nSwipe down to refresh" else null);
setPager(cachePager);
} else {
setTextCentered(null);
}
_taskGetPager.run(withRefetch);
}
@@ -294,6 +299,7 @@ class SubscriptionsFeedFragment : MainFragment() {
finishRefreshLayoutLoader();
setLoading(false);
setPager(pager);
setTextCentered(if (pager.getResults().isEmpty()) "No results found\nSwipe down to refresh" else null);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to finish loading", e)
}
@@ -873,7 +873,7 @@ class VideoDetailView : ConstraintLayout {
_channelName.text = video.author.name;
_playWhenReady = true;
if(video.author.subscribers != null) {
_channelMeta.text = video.author.subscribers!!.toHumanNumber() + " subscribers";
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
} else {
_channelMeta.text = "";
@@ -982,7 +982,7 @@ class VideoDetailView : ConstraintLayout {
_title.text = video.name;
_channelName.text = video.author.name;
if(video.author.subscribers != null) {
_channelMeta.text = video.author.subscribers!!.toHumanNumber() + " subscribers";
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
} else {
_channelMeta.text = "";
@@ -57,10 +57,8 @@ abstract class VideoListEditorView : LinearLayout {
buttonPlayAll.setOnClickListener { onPlayAllClick(); };
buttonShuffle.setOnClickListener { onShuffleClick(); };
if (canEdit())
_buttonEdit.setOnClickListener { onEditClick(); };
else
_buttonEdit.visibility = View.GONE;
_buttonEdit.setOnClickListener { onEditClick(); };
setButtonDownloadVisible(canEdit());
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
videoListEditorView.onVideoRemoved.subscribe(::onVideoRemoved);
@@ -61,9 +61,21 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
_deferred.invokeOnCompletion(throwable -> {
if (throwable != null) {
callback.onLoadFailed(new Exception(throwable));
return Unit.INSTANCE;
}
Deferred<ByteBuffer> deferred = _deferred;
if (deferred == null) {
callback.onLoadFailed(new Exception("Deferred is null"));
return Unit.INSTANCE;
}
ByteBuffer completed = deferred.getCompleted();
if (completed != null) {
callback.onDataReady(completed);
} else {
callback.onLoadFailed(new Exception("Completed is null"));
}
final ByteBuffer completed = _deferred.getCompleted();
callback.onDataReady(completed);
return Unit.INSTANCE;
});
}
@@ -2,6 +2,7 @@ package com.futo.platformplayer.others
import android.net.Uri
import android.webkit.*
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.constructs.Event1
@@ -43,6 +44,8 @@ class LoginWebViewClient : WebViewClient {
private var urlFound = false;
override fun onPageFinished(view: WebView?, url: String?) {
if(BuildConfig.DEBUG)
Logger.i(TAG, "Login Url Page: " + url);
super.onPageFinished(view, url);
onPageLoaded.emit(view, url);
}
@@ -56,11 +59,29 @@ class LoginWebViewClient : WebViewClient {
return null;
}
var completionUrlExcludeQuery = false
var completionUrlToCheck = if(urlFound) null else _authConfig.completionUrl;
if(completionUrlToCheck != null) {
if(completionUrlToCheck.endsWith("?*")) {
completionUrlToCheck = completionUrlToCheck.substring(0, completionUrlToCheck.length - 2);
completionUrlExcludeQuery = true;
}
}
val domain = request.url.host;
val domainLower = request.url.host?.lowercase();
val urlString = request.url.toString();
if(_authConfig.completionUrl == null)
urlFound = true;
else urlFound = urlFound || request.url == Uri.parse(_authConfig.completionUrl);
else urlFound = urlFound || (
if(completionUrlExcludeQuery)
(if(urlString.contains("?"))
urlString.substring(0, urlString.indexOf("?")) == completionUrlToCheck
else urlString == completionUrlToCheck)
else
request.url == Uri.parse(_authConfig.completionUrl)
);
//HEADERS
if(domainLower != null) {
@@ -29,6 +29,7 @@ import com.futo.platformplayer.activities.CaptchaActivity
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.background.BackgroundWorker
@@ -672,13 +673,20 @@ class StateApp {
UIDialogs.showConfirmationDialog(context, "Captcha required\nPlugin [${client.config.name}]", {
CaptchaActivity.showCaptcha(context, client.config, exception.url, exception.body) {
hasCaptchaDialog = false;
StatePlugins.instance.setPluginCaptcha(client.config.id, it);
scopeOrNull?.launch(Dispatchers.IO) {
try {
StatePlatform.instance.reloadClient(context, client.config.id);
} catch (e: Throwable) {
Logger.e(SourceDetailFragment.TAG, "Failed to reload client.", e)
return@launch;
if(client is DevJSClient) {
client.setCaptcha(it);
client.recreate(context);
}
else {
StatePlugins.instance.setPluginCaptcha(client.config.id, it);
scopeOrNull?.launch(Dispatchers.IO) {
try {
StatePlatform.instance.reloadClient(context, client.config.id);
} catch (e: Throwable) {
Logger.e(SourceDetailFragment.TAG, "Failed to reload client.", e)
return@launch;
}
}
}
}
@@ -16,6 +16,7 @@ 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
@@ -320,7 +321,16 @@ class StateSubscriptions {
synchronized(failedPlugins) {
//Fail all subscription calls to plugin if it has a captcha issue
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
Logger.w(TAG, "Subscriptions fetch ignoring plugin [${ex.config.name}] due to Captcha");
Logger.w(TAG, "Subscriptionsgnoring plugin [${ex.config.name}] due to Captcha");
failedPlugins.add(ex.config.id);
}
}
}
else if(ex is ScriptCriticalException) {
synchronized(failedPlugins) {
//Fail all subscription calls to plugin if it has a critical issue
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
Logger.w(TAG, "Subscriptions ignoring plugin [${ex.config.name}] due to critical exception:\n" + ex.message);
failedPlugins.add(ex.config.id);
}
}
@@ -50,7 +50,7 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L)
_textMetadata.visibility = View.GONE;
else {
_textMetadata.text = authorLink.subscribers!!.toHumanNumber() + " subscribers";
_textMetadata.text = if(authorLink?.subscribers ?: 0 > 0) authorLink.subscribers!!.toHumanNumber() + " subscribers" else "";
_textMetadata.visibility = View.VISIBLE;
}
_buttonSubscribe.setSubscribeChannel(authorLink.url);
@@ -14,13 +14,13 @@ class BigButtonGroup : LinearLayout {
_header = findViewById(R.id.header_title);
_buttons = findViewById(R.id.buttons);
}
constructor(context: Context, header: String, vararg buttons: BigButton) : super(context) {
constructor(context: Context, header: String, vararg buttons: BigButton?) : super(context) {
inflate(context, R.layout.big_button_group, this);
_header = findViewById(R.id.header_title);
_buttons = findViewById(R.id.buttons);
_header.text = header;
for(button in buttons)
for(button in buttons.filterNotNull())
_buttons.addView(button);
}
@@ -69,6 +69,7 @@
android:layout_marginRight="30dp"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textPassword"
android:hint="Backup Password" />
<LinearLayout
@@ -51,6 +51,7 @@
android:layout_marginTop="10dp"
android:layout_marginRight="30dp"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"
android:hint="Backup Password" />
+27 -12
View File
@@ -76,22 +76,37 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:orientation="vertical"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.futo.platformplayer.views.others.ProgressBar
android:id="@+id/progress_bar"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp" />
android:layout_height="wrap_content">
<com.futo.platformplayer.views.others.ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="0dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</LinearLayout>
<TextView
android:id="@+id/text_centered"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:fontFamily="@font/inter_regular"
android:gravity="center"
android:textColor="@color/gray_ac"
android:textSize="12dp" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>