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
362c7f5b2c
14 changed files with 280 additions and 87 deletions
|
@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
|
||||||
function pluginRemoteCall(objID, methodName, args) {
|
function pluginRemoteCall(objID, methodName, args) {
|
||||||
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
||||||
}
|
}
|
||||||
|
function pluginRemoteTest(methodName, args) {
|
||||||
|
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
|
||||||
|
}
|
||||||
|
|
||||||
function pluginIsLoggedIn(cb, err) {
|
function pluginIsLoggedIn(cb, err) {
|
||||||
fetch("/plugin/isLoggedIn", {
|
fetch("/plugin/isLoggedIn", {
|
||||||
|
|
|
@ -385,8 +385,8 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||||
<!--Get Home-->
|
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
|
||||||
<v-card class="requestCard" v-for="req in Testing.requests">
|
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<span v-if="req.isOptional">(Optional)</span>
|
<span v-if="req.isOptional">(Optional)</span>
|
||||||
|
@ -416,6 +416,9 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="testSourceRemotely(req)">
|
||||||
|
Test Android
|
||||||
|
</v-btn>
|
||||||
<v-btn @click="testSource(req)">
|
<v-btn @click="testSource(req)">
|
||||||
Test
|
Test
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -545,6 +548,7 @@
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
searchTestMethods: "",
|
||||||
page: "Plugin",
|
page: "Plugin",
|
||||||
pastPluginUrls: [],
|
pastPluginUrls: [],
|
||||||
settings: {},
|
settings: {},
|
||||||
|
@ -860,6 +864,53 @@
|
||||||
"Error: " + ex;
|
"Error: " + ex;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
testSourceRemotely(req) {
|
||||||
|
const name = req.title;
|
||||||
|
const parameterVals = req.parameters.map(x=>{
|
||||||
|
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
|
||||||
|
return JSON.parse(x.value.substring(5));
|
||||||
|
return x.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if(name == "enable") {
|
||||||
|
if(parameterVals.length > 0)
|
||||||
|
parameterVals[0] = this.Plugin.currentPlugin;
|
||||||
|
else
|
||||||
|
parameterVals.push(this.Plugin.currentPlugin);
|
||||||
|
if(parameterVals.length > 1)
|
||||||
|
parameterVals[1] = __DEV_SETTINGS;
|
||||||
|
else
|
||||||
|
parameterVals.push(__DEV_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const func = source[name];
|
||||||
|
if(!func)
|
||||||
|
alert("Test func not found");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const remoteResult = pluginRemoteTest(name, parameterVals);
|
||||||
|
console.log("Result for " + req.title, remoteResult);
|
||||||
|
this.Testing.lastResult = "//Results [" + name + "]\n" +
|
||||||
|
JSON.stringify(remoteResult, null, 3);
|
||||||
|
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)
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex.message + "\n\n" + ex.stack;
|
||||||
|
else
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex;
|
||||||
|
}
|
||||||
|
},
|
||||||
showTestResults(results) {
|
showTestResults(results) {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
|
@ -141,7 +142,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
runBlocking {
|
||||||
handleUrlAll(content)
|
handleUrlAll(content)
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.i(TAG, "Failed to handle URL.", e)
|
Logger.i(TAG, "Failed to handle URL.", e)
|
||||||
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||||
|
@ -540,8 +543,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
Pair("grayjay") { req ->
|
Pair("grayjay") { req ->
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
if(it is MainActivity) {
|
if(it is MainActivity) {
|
||||||
|
runBlocking {
|
||||||
it.handleUrlAll(req.url.toString());
|
it.handleUrlAll(req.url.toString());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)));
|
)));
|
||||||
|
@ -552,15 +557,17 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
|
runBlocking {
|
||||||
handleUrlAll(targetData)
|
handleUrlAll(targetData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrlAll(url: String) {
|
suspend fun handleUrlAll(url: String) {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
|
@ -644,31 +651,38 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrl(url: String): Boolean {
|
suspend fun handleUrl(url: String): Boolean {
|
||||||
Logger.i(TAG, "handleUrl(url=$url)")
|
Logger.i(TAG, "handleUrl(url=$url)")
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||||
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigate(_fragVideoDetail, url);
|
navigate(_fragVideoDetail, url);
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
|
||||||
return true;
|
|
||||||
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) {
|
|
||||||
navigate(_fragMainChannel, url);
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
|
}
|
||||||
|
return@withContext true;
|
||||||
|
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||||
|
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
navigate(_fragMainChannel, url);
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
return true;
|
return@withContext true;
|
||||||
}
|
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||||
else if(StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigate(_fragMainPlaylist, url);
|
navigate(_fragMainPlaylist, url);
|
||||||
lifecycleScope.launch {
|
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
return true;
|
return@withContext true;
|
||||||
|
}
|
||||||
|
return@withContext false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||||
Logger.i(TAG, "handleContent(url=$file)");
|
Logger.i(TAG, "handleContent(url=$file)");
|
||||||
|
|
|
@ -9,7 +9,10 @@ import com.futo.platformplayer.api.http.server.HttpGET
|
||||||
import com.futo.platformplayer.api.http.server.HttpPOST
|
import com.futo.platformplayer.api.http.server.HttpPOST
|
||||||
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.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.internal.JSDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||||
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
||||||
|
@ -20,18 +23,29 @@ import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.google.gson.ExclusionStrategy
|
||||||
|
import com.google.gson.FieldAttributes
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import kotlin.reflect.full.memberFunctions
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
import kotlin.reflect.jvm.jvmErasure
|
import kotlin.reflect.jvm.jvmErasure
|
||||||
|
|
||||||
class DeveloperEndpoints(private val context: Context) {
|
class DeveloperEndpoints(private val context: Context) {
|
||||||
private val TAG = "DeveloperEndpoints";
|
private val TAG = "DeveloperEndpoints";
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
private var _testPlugin: V8Plugin? = null;
|
private var _testPlugin: V8Plugin? = null;
|
||||||
|
private var _testPluginFull: JSClient? = null;
|
||||||
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
||||||
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||||
|
|
||||||
|
@ -190,6 +204,17 @@ class DeveloperEndpoints(private val context: Context) {
|
||||||
val client = JSHttpClient(null, null, null, config);
|
val client = JSHttpClient(null, null, null, config);
|
||||||
val clientAuth = JSHttpClient(null, null, null, config);
|
val clientAuth = JSHttpClient(null, null, null, config);
|
||||||
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
|
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
|
||||||
|
try {
|
||||||
|
val script = _client.get(config.absoluteScriptUrl);
|
||||||
|
_testPluginFull = JSClient(StateApp.instance.context, SourcePluginDescriptor(
|
||||||
|
config, null, null, null
|
||||||
|
), null, script.body?.string() ?: "");
|
||||||
|
_testPluginFull!!.initialize();
|
||||||
|
}
|
||||||
|
catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Loading full client failed", ex);
|
||||||
|
_testPluginFull = null;
|
||||||
|
}
|
||||||
|
|
||||||
context.respondJson(200, testPluginOrThrow.getPackageVariables());
|
context.respondJson(200, testPluginOrThrow.getPackageVariables());
|
||||||
}
|
}
|
||||||
|
@ -440,6 +465,68 @@ class DeveloperEndpoints(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _fieldAttributesField = FieldAttributes::class.java.getDeclaredField("field");
|
||||||
|
init {
|
||||||
|
_fieldAttributesField.isAccessible = true;
|
||||||
|
}
|
||||||
|
private val _remoteTestGson = GsonBuilder()
|
||||||
|
.setExclusionStrategies(object : ExclusionStrategy {
|
||||||
|
override fun shouldSkipClass(clazz: Class<*>?): Boolean {
|
||||||
|
return clazz?.simpleName == "JSClient" ||
|
||||||
|
clazz?.simpleName == "KSerializer[]" ||
|
||||||
|
clazz?.simpleName == "V8ValueObject";
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldSkipField(f: FieldAttributes?): Boolean {
|
||||||
|
val isPublic = f?.hasModifier(Modifier.PUBLIC) ?: true;
|
||||||
|
if(!isPublic) {
|
||||||
|
val underlyingField = _fieldAttributesField.get(f) as Field;
|
||||||
|
return !(underlyingField.declaringClass as Class).methods.any { it.name == "get" + underlyingField.name.replaceFirstChar { it.uppercaseChar() } && Modifier.isPublic(it.modifiers) };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return !isPublic;
|
||||||
|
}
|
||||||
|
}).create();
|
||||||
|
@HttpPOST("/plugin/remoteTest")
|
||||||
|
fun pluginRemoteTest(context: HttpContext) {
|
||||||
|
val method = context.query.getOrDefault("method", "");
|
||||||
|
try {
|
||||||
|
|
||||||
|
val parameters = context.readContentString();
|
||||||
|
val paras = JsonParser.parseString(parameters);
|
||||||
|
if(!paras.isJsonArray)
|
||||||
|
throw IllegalArgumentException("Expected json array as body");
|
||||||
|
|
||||||
|
val plugin = _testPluginFull ?: throw IllegalStateException("Plugin not loaded");
|
||||||
|
|
||||||
|
val function = plugin::class.memberFunctions.filter { it.findAnnotation<JSDocs>() != null }
|
||||||
|
.find { it.name == method };
|
||||||
|
if(function == null)
|
||||||
|
throw java.lang.IllegalArgumentException("Plugin method [${function}] not found");
|
||||||
|
val callResult = function.call(*(listOf(plugin) + paras.asJsonArray.take(function.parameters.size - 1).mapIndexed { index, jsonElement ->
|
||||||
|
//For now, manual conversion.
|
||||||
|
val parameter = function.parameters[index + 1];
|
||||||
|
val value = _remoteTestGson.fromJson<Any>(jsonElement, parameter.type.javaType);
|
||||||
|
return@mapIndexed value;
|
||||||
|
}).toTypedArray());
|
||||||
|
val json = if(callResult is IPager<*>)
|
||||||
|
_remoteTestGson.toJson(callResult.getResults())
|
||||||
|
else
|
||||||
|
_remoteTestGson.toJson(callResult);
|
||||||
|
//val json = wrapRemoteResult(callResult, false);
|
||||||
|
|
||||||
|
context.respondCode(200, json);
|
||||||
|
}
|
||||||
|
catch(ex: InvocationTargetException) {
|
||||||
|
Logger.e(TAG, "Remote test for [${method}] is failed", ex.targetException);
|
||||||
|
context.respondCode(500, ex.targetException.message ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.e(TAG, "Remote test for [${method}] is failed", ex);
|
||||||
|
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Internal calls
|
//Internal calls
|
||||||
@HttpPOST("/get")
|
@HttpPOST("/get")
|
||||||
fun get(context: HttpContext) {
|
fun get(context: HttpContext) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.engine
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||||
|
import com.caoccao.javet.exceptions.JavetException
|
||||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||||
import com.caoccao.javet.interop.V8Host
|
import com.caoccao.javet.interop.V8Host
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
|
@ -10,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
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.UIDialogs
|
||||||
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.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
@ -173,8 +175,16 @@ class V8Plugin {
|
||||||
isStopped = true;
|
isStopped = true;
|
||||||
_runtime?.let {
|
_runtime?.let {
|
||||||
_runtime = null;
|
_runtime = null;
|
||||||
if(!it.isClosed && !it.isDead)
|
if(!it.isClosed && !it.isDead) {
|
||||||
|
try {
|
||||||
it.close();
|
it.close();
|
||||||
|
}
|
||||||
|
catch(ex: JavetException) {
|
||||||
|
//In case race conditions are going on, already closed runtimes are fine.
|
||||||
|
if(ex.message?.contains("Runtime is already closed") != true)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@ -168,8 +171,10 @@ class HomeFragment : MainFragment() {
|
||||||
Pair("grayjay") { req ->
|
Pair("grayjay") { req ->
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
if(it is MainActivity) {
|
if(it is MainActivity) {
|
||||||
|
runBlocking {
|
||||||
it.handleUrlAll(req.url.toString());
|
it.handleUrlAll(req.url.toString());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)));
|
)));
|
||||||
|
|
|
@ -859,11 +859,11 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
|
|
||||||
private val _historyIndexLock = Mutex(false);
|
private val _historyIndexLock = Mutex(false);
|
||||||
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
|
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index? = withContext(Dispatchers.IO){
|
||||||
_historyIndexLock.withLock {
|
_historyIndexLock.withLock {
|
||||||
val current = _historyIndex;
|
val current = _historyIndex;
|
||||||
if(current == null || current.url != video.url) {
|
if(current == null || current.url != video.url) {
|
||||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
val index = StateHistory.instance.getHistoryByVideo(video, true);
|
||||||
_historyIndex = index;
|
_historyIndex = index;
|
||||||
return@withContext index;
|
return@withContext index;
|
||||||
}
|
}
|
||||||
|
@ -1390,7 +1390,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
if (video !is TutorialFragment.TutorialVideo) {
|
if (video !is TutorialFragment.TutorialVideo) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val historyItem = getHistoryIndex(videoDetail);
|
val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||||
|
@ -2252,7 +2252,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||||
if (v !is TutorialFragment.TutorialVideo) {
|
if (v !is TutorialFragment.TutorialVideo) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val history = getHistoryIndex(v);
|
val history = getHistoryIndex(v) ?: return@launch;
|
||||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.timestampRegex
|
import com.futo.platformplayer.timestampRegex
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||||
private val _context: Context;
|
private val _context: Context;
|
||||||
|
@ -32,6 +33,7 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||||
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
||||||
|
|
||||||
if (links.isNotEmpty()) {
|
if (links.isNotEmpty()) {
|
||||||
|
runBlocking {
|
||||||
for (link in links) {
|
for (link in links) {
|
||||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||||
|
|
||||||
|
@ -47,7 +49,8 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||||
if (tokens.size == 2) {
|
if (tokens.size == 2) {
|
||||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||||
} else if (tokens.size == 3) {
|
} else if (tokens.size == 3) {
|
||||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
time_s =
|
||||||
|
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (time_s != -1L) {
|
if (time_s != -1L) {
|
||||||
|
@ -60,6 +63,7 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||||
|
|
||||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
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.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
@ -92,14 +93,20 @@ class StateHistory {
|
||||||
}
|
}
|
||||||
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
||||||
val existing = historyIndex[video.url];
|
val existing = historyIndex[video.url];
|
||||||
if(existing != null)
|
var result: DBHistory.Index? = null;
|
||||||
return _historyDBStore.get(existing.id!!);
|
if(existing != null) {
|
||||||
|
result = _historyDBStore.getOrNull(existing.id!!);
|
||||||
|
if(result == null)
|
||||||
|
UIDialogs.toast("History item null?\nNo history tracking..");
|
||||||
|
}
|
||||||
else if(create) {
|
else if(create) {
|
||||||
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
||||||
val id = _historyDBStore.insert(newHistItem);
|
val id = _historyDBStore.insert(newHistItem);
|
||||||
return _historyDBStore.get(id);
|
result = _historyDBStore.getOrNull(id);
|
||||||
|
if(result == null)
|
||||||
|
UIDialogs.toast("History creation failed?\nNo history tracking..");
|
||||||
}
|
}
|
||||||
return null;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeHistory(url: String) {
|
fun removeHistory(url: String) {
|
||||||
|
|
|
@ -46,6 +46,7 @@ import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -166,9 +167,14 @@ class StatePlatform {
|
||||||
var enabled: Array<String>;
|
var enabled: Array<String>;
|
||||||
synchronized(_clientsLock) {
|
synchronized(_clientsLock) {
|
||||||
for(e in _enabledClients) {
|
for(e in _enabledClients) {
|
||||||
|
try {
|
||||||
e.disable();
|
e.disable();
|
||||||
onSourceDisabled.emit(e);
|
onSourceDisabled.emit(e);
|
||||||
}
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_enabledClients.clear();
|
_enabledClients.clear();
|
||||||
_availableClients.clear();
|
_availableClients.clear();
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
import java.lang.IllegalArgumentException
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ConcurrentMap
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
@ -209,7 +210,9 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
||||||
|
|
||||||
fun getObject(id: Long) = get(id).obj!!;
|
fun getObject(id: Long) = get(id).obj!!;
|
||||||
fun get(id: Long): I {
|
fun get(id: Long): I {
|
||||||
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
|
val result = dbDaoBase.getNullable(_sqlGet(id))
|
||||||
|
?: throw IllegalArgumentException("DB [${name}] has no entry with id ${id}");
|
||||||
|
return deserializeIndex(result);
|
||||||
}
|
}
|
||||||
fun getOrNull(id: Long): I? {
|
fun getOrNull(id: Long): I? {
|
||||||
val result = dbDaoBase.getNullable(_sqlGet(id));
|
val result = dbDaoBase.getNullable(_sqlGet(id));
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.timestampRegex
|
import com.futo.platformplayer.timestampRegex
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||||
constructor(context: Context) : super(context) {}
|
constructor(context: Context) : super(context) {}
|
||||||
|
@ -40,6 +41,7 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||||
if (text is Spannable) {
|
if (text is Spannable) {
|
||||||
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
||||||
if (links.isNotEmpty()) {
|
if (links.isNotEmpty()) {
|
||||||
|
runBlocking {
|
||||||
for (link in links) {
|
for (link in links) {
|
||||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||||
|
|
||||||
|
@ -68,6 +70,7 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 263ed8c7dfea3915f4981b6dae0999ff02815f03
|
Subproject commit bedbc4a9891913e0bfc06b94383a89478274e79d
|
|
@ -1 +1 @@
|
||||||
Subproject commit 263ed8c7dfea3915f4981b6dae0999ff02815f03
|
Subproject commit bedbc4a9891913e0bfc06b94383a89478274e79d
|
Loading…
Add table
Add a link
Reference in a new issue