mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +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) {
|
||||
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) {
|
||||
fetch("/plugin/isLoggedIn", {
|
||||
|
|
|
@ -385,8 +385,8 @@
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||
<!--Get Home-->
|
||||
<v-card class="requestCard" v-for="req in Testing.requests">
|
||||
<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-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||
<v-card-text>
|
||||
<div class="title">
|
||||
<span v-if="req.isOptional">(Optional)</span>
|
||||
|
@ -416,6 +416,9 @@
|
|||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="testSourceRemotely(req)">
|
||||
Test Android
|
||||
</v-btn>
|
||||
<v-btn @click="testSource(req)">
|
||||
Test
|
||||
</v-btn>
|
||||
|
@ -545,6 +548,7 @@
|
|||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
searchTestMethods: "",
|
||||
page: "Plugin",
|
||||
pastPluginUrls: [],
|
||||
settings: {},
|
||||
|
@ -860,6 +864,53 @@
|
|||
"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) {
|
||||
|
||||
},
|
||||
|
|
|
@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentContainerView
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
|
@ -141,7 +142,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
}
|
||||
|
||||
try {
|
||||
handleUrlAll(content)
|
||||
runBlocking {
|
||||
handleUrlAll(content)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to handle URL.", e)
|
||||
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||
|
@ -540,7 +543,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
Pair("grayjay") { req ->
|
||||
StateApp.instance.contextOrNull?.let {
|
||||
if(it is MainActivity) {
|
||||
it.handleUrlAll(req.url.toString());
|
||||
runBlocking {
|
||||
it.handleUrlAll(req.url.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -552,7 +557,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
|
||||
try {
|
||||
if (targetData != null) {
|
||||
handleUrlAll(targetData)
|
||||
runBlocking {
|
||||
handleUrlAll(targetData)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
|
@ -560,7 +567,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
}
|
||||
}
|
||||
|
||||
fun handleUrlAll(url: String) {
|
||||
suspend fun handleUrlAll(url: String) {
|
||||
val uri = Uri.parse(url)
|
||||
when (uri.scheme) {
|
||||
"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)")
|
||||
|
||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||
navigate(_fragVideoDetail, url);
|
||||
_fragVideoDetail.maximizeVideoDetail(true);
|
||||
return true;
|
||||
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||
navigate(_fragMainChannel, url);
|
||||
return withContext(Dispatchers.IO) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
navigate(_fragVideoDetail, url);
|
||||
|
||||
lifecycleScope.launch {
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return true;
|
||||
_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);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return@withContext true;
|
||||
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
navigate(_fragMainPlaylist, url);
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return@withContext true;
|
||||
}
|
||||
return@withContext false;
|
||||
}
|
||||
else if(StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||
navigate(_fragMainPlaylist, url);
|
||||
lifecycleScope.launch {
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||
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.media.platforms.js.JSClient
|
||||
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.structures.IPager
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||
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.StateDeveloper
|
||||
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.JsonElement
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Modifier
|
||||
import java.util.UUID
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberFunctions
|
||||
import kotlin.reflect.jvm.javaType
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
class DeveloperEndpoints(private val context: Context) {
|
||||
private val TAG = "DeveloperEndpoints";
|
||||
private val _client = ManagedHttpClient();
|
||||
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 _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||
|
||||
|
@ -190,6 +204,17 @@ class DeveloperEndpoints(private val context: Context) {
|
|||
val client = JSHttpClient(null, null, null, config);
|
||||
val clientAuth = JSHttpClient(null, null, null, config);
|
||||
_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());
|
||||
}
|
||||
|
@ -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
|
||||
@HttpPOST("/get")
|
||||
fun get(context: HttpContext) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.engine
|
|||
|
||||
import android.content.Context
|
||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||
import com.caoccao.javet.exceptions.JavetException
|
||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||
import com.caoccao.javet.interop.V8Host
|
||||
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.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
|
@ -173,8 +175,16 @@ class V8Plugin {
|
|||
isStopped = true;
|
||||
_runtime?.let {
|
||||
_runtime = null;
|
||||
if(!it.isClosed && !it.isDead)
|
||||
it.close();
|
||||
if(!it.isClosed && !it.isDead) {
|
||||
try {
|
||||
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}]");
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,6 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||
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.util.UUID
|
||||
|
||||
|
@ -168,7 +171,9 @@ class HomeFragment : MainFragment() {
|
|||
Pair("grayjay") { req ->
|
||||
StateApp.instance.contextOrNull?.let {
|
||||
if(it is MainActivity) {
|
||||
it.handleUrlAll(req.url.toString());
|
||||
runBlocking {
|
||||
it.handleUrlAll(req.url.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -859,11 +859,11 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
|
||||
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 {
|
||||
val current = _historyIndex;
|
||||
if(current == null || current.url != video.url) {
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true);
|
||||
_historyIndex = index;
|
||||
return@withContext index;
|
||||
}
|
||||
|
@ -1390,7 +1390,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_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 (v !is TutorialFragment.TutorialVideo) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.futo.platformplayer.activities.MainActivity
|
|||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||
private val _context: Context;
|
||||
|
@ -32,33 +33,36 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
|||
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
||||
|
||||
if (links.isNotEmpty()) {
|
||||
for (link in links) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||
runBlocking {
|
||||
for (link in links) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||
|
||||
if (_context is MainActivity) {
|
||||
if (_context.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
if (_context is MainActivity) {
|
||||
if (_context.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s =
|
||||
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
|
||||
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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.SerializedPlatformVideo
|
||||
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? {
|
||||
val existing = historyIndex[video.url];
|
||||
if(existing != null)
|
||||
return _historyDBStore.get(existing.id!!);
|
||||
var result: DBHistory.Index? = null;
|
||||
if(existing != null) {
|
||||
result = _historyDBStore.getOrNull(existing.id!!);
|
||||
if(result == null)
|
||||
UIDialogs.toast("History item null?\nNo history tracking..");
|
||||
}
|
||||
else if(create) {
|
||||
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
||||
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) {
|
||||
|
|
|
@ -46,6 +46,7 @@ import com.futo.platformplayer.models.ImageVariable
|
|||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -166,8 +167,13 @@ class StatePlatform {
|
|||
var enabled: Array<String>;
|
||||
synchronized(_clientsLock) {
|
||||
for(e in _enabledClients) {
|
||||
e.disable();
|
||||
onSourceDisabled.emit(e);
|
||||
try {
|
||||
e.disable();
|
||||
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();
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.futo.platformplayer.states.StateApp
|
|||
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||
import kotlinx.serialization.KSerializer
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.reflect.Field
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
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 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? {
|
||||
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.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
constructor(context: Context) : super(context) {}
|
||||
|
@ -40,32 +41,34 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
|||
if (text is Spannable) {
|
||||
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
||||
if (links.isNotEmpty()) {
|
||||
for (link in links) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||
runBlocking {
|
||||
for (link in links) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||
|
||||
val c = context;
|
||||
if (c is MainActivity) {
|
||||
if (c.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
val c = context;
|
||||
if (c is MainActivity) {
|
||||
if (c.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 263ed8c7dfea3915f4981b6dae0999ff02815f03
|
||||
Subproject commit bedbc4a9891913e0bfc06b94383a89478274e79d
|
|
@ -1 +1 @@
|
|||
Subproject commit 263ed8c7dfea3915f4981b6dae0999ff02815f03
|
||||
Subproject commit bedbc4a9891913e0bfc06b94383a89478274e79d
|
Loading…
Add table
Reference in a new issue