mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-03 14:50:49 +00:00
Plugin auto-update support and prompting
This commit is contained in:
parent
b4fddbe26a
commit
d44df42727
30 changed files with 801 additions and 167 deletions
|
@ -18,6 +18,7 @@ import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||||
|
@ -31,12 +32,17 @@ import com.futo.platformplayer.dialogs.ConnectedCastingDialog
|
||||||
import com.futo.platformplayer.dialogs.ImportDialog
|
import com.futo.platformplayer.dialogs.ImportDialog
|
||||||
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
||||||
import com.futo.platformplayer.dialogs.MigrateDialog
|
import com.futo.platformplayer.dialogs.MigrateDialog
|
||||||
|
import com.futo.platformplayer.dialogs.PluginUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.views.ToastView
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -184,6 +190,14 @@ class UIDialogs {
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showPluginUpdateDialog(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) {
|
||||||
|
val dialog = PluginUpdateDialog(context, oldConfig, newConfig);
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||||
val builder = AlertDialog.Builder(context);
|
val builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
|
@ -269,22 +283,48 @@ class UIDialogs {
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null, mainFragment: MainFragment? = null) {
|
||||||
|
val pluginConfig = if(ex is PluginException) ex.config else null;
|
||||||
val pluginInfo = if(ex is PluginException)
|
val pluginInfo = if(ex is PluginException)
|
||||||
"\nPlugin [${ex.config.name}]" else "";
|
"\nPlugin [${ex.config.name}]" else "";
|
||||||
showDialog(context,
|
|
||||||
R.drawable.ic_error_pred,
|
var exMsg = if(ex != null ) "${ex.message}" else "";
|
||||||
"${msg}${pluginInfo}",
|
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||||
(if(ex != null ) "${ex.message}" else ""),
|
exMsg += "\n\nAn update is available"
|
||||||
if(ex is PluginException) ex.code else null,
|
|
||||||
0,
|
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||||
UIDialogs.Action(context.getString(R.string.retry), {
|
showDialog(context,
|
||||||
retryAction?.invoke();
|
R.drawable.ic_error_pred,
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
"${msg}${pluginInfo}",
|
||||||
UIDialogs.Action(context.getString(R.string.close), {
|
exMsg,
|
||||||
closeAction?.invoke()
|
if(ex is PluginException) ex.code else null,
|
||||||
}, UIDialogs.ActionStyle.NONE)
|
1,
|
||||||
);
|
UIDialogs.Action(context.getString(R.string.update), {
|
||||||
|
mainFragment.navigate<SourceDetailFragment>(pluginConfig);
|
||||||
|
if(mainFragment is VideoDetailFragment)
|
||||||
|
mainFragment.minimizeVideoDetail();
|
||||||
|
}, UIDialogs.ActionStyle.ACCENT),
|
||||||
|
UIDialogs.Action(context.getString(R.string.close), {
|
||||||
|
closeAction?.invoke()
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
|
retryAction?.invoke();
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
showDialog(context,
|
||||||
|
R.drawable.ic_error_pred,
|
||||||
|
"${msg}${pluginInfo}",
|
||||||
|
exMsg,
|
||||||
|
if(ex is PluginException) ex.code else null,
|
||||||
|
0,
|
||||||
|
UIDialogs.Action(context.getString(R.string.close), {
|
||||||
|
closeAction?.invoke()
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
|
retryAction?.invoke();
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
||||||
|
|
|
@ -224,7 +224,7 @@ class AddSourceActivity : AppCompatActivity() {
|
||||||
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||||
if(it) {
|
if(it) {
|
||||||
StatePlatform.instance.clearUpdateAvailable(config)
|
StatePlugins.instance.clearUpdateAvailable(config)
|
||||||
if(isNew)
|
if(isNew)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
StatePlatform.instance.enableClient(listOf(config.id));
|
StatePlatform.instance.enableClient(listOf(config.id));
|
||||||
|
|
|
@ -46,6 +46,7 @@ import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
@ -56,6 +57,7 @@ import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.lang.Exception
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.reflect.full.findAnnotations
|
import kotlin.reflect.full.findAnnotations
|
||||||
import kotlin.reflect.jvm.kotlinFunction
|
import kotlin.reflect.jvm.kotlinFunction
|
||||||
|
|
|
@ -80,6 +80,41 @@ class SourcePluginConfig(
|
||||||
return _allowUrlsLowerVal!!;
|
return _allowUrlsLowerVal!!;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fun isLowRiskUpdate(oldScript: String, newConfig: SourcePluginConfig, newScript: String): Boolean{
|
||||||
|
|
||||||
|
//All urls should already be allowed
|
||||||
|
for(url in newConfig.allowUrls) {
|
||||||
|
if(!allowUrls.contains(url))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//All packages should already be allowed
|
||||||
|
for(pack in newConfig.packages) {
|
||||||
|
if(!packages.contains(pack))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//Developer Submit Url should be same or empty
|
||||||
|
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Should have a public key
|
||||||
|
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Should be same public key
|
||||||
|
if(scriptPublicKey != newConfig.scriptPublicKey)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Old signature should be valid
|
||||||
|
if(!validate(oldScript))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//New signature should be valid
|
||||||
|
if(!newConfig.validate(newScript))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
||||||
val list = mutableListOf<Pair<String,String>>();
|
val list = mutableListOf<Pair<String,String>>();
|
||||||
|
|
||||||
|
|
|
@ -91,8 +91,10 @@ class SourcePluginDescriptor {
|
||||||
@Serializable
|
@Serializable
|
||||||
class AppPluginSettings {
|
class AppPluginSettings {
|
||||||
|
|
||||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
|
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
|
||||||
var checkForUpdates: Boolean = true;
|
var checkForUpdates: Boolean = true;
|
||||||
|
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
|
||||||
|
var automaticUpdate: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||||
var tabEnabled = TabEnabled();
|
var tabEnabled = TabEnabled();
|
||||||
|
|
|
@ -0,0 +1,253 @@
|
||||||
|
package com.futo.platformplayer.dialogs
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.media.MediaCas.PluginDescriptor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.method.ScrollingMovementMethod
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.AddSourceActivity
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class PluginUpdateDialog : AlertDialog {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "PluginUpdateDialog";
|
||||||
|
}
|
||||||
|
private val _context: Context;
|
||||||
|
|
||||||
|
private lateinit var _buttonCancel1: Button;
|
||||||
|
private lateinit var _buttonCancel2: Button;
|
||||||
|
private lateinit var _buttonUpdate: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _buttonOk: LinearLayout;
|
||||||
|
private lateinit var _buttonInstall: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _textPlugin: TextView;
|
||||||
|
private lateinit var _textProgres: TextView;
|
||||||
|
private lateinit var _textError: TextView;
|
||||||
|
private lateinit var _textResult: TextView;
|
||||||
|
|
||||||
|
private lateinit var _uiChoiceTop: FrameLayout;
|
||||||
|
private lateinit var _uiProgressTop: FrameLayout;
|
||||||
|
private lateinit var _uiRiskTop: FrameLayout;
|
||||||
|
|
||||||
|
private lateinit var _uiChoiceBot: LinearLayout;
|
||||||
|
private lateinit var _uiResultBot: LinearLayout;
|
||||||
|
private lateinit var _uiRiskBot: LinearLayout;
|
||||||
|
private lateinit var _uiProgressBot: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _iconPlugin: ImageView;
|
||||||
|
private lateinit var _updateSpinner: ImageView;
|
||||||
|
|
||||||
|
private var _isUpdating = false;
|
||||||
|
|
||||||
|
private val _oldConfig: SourcePluginConfig;
|
||||||
|
private val _newConfig: SourcePluginConfig;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): super(context) {
|
||||||
|
_context = context;
|
||||||
|
_oldConfig = oldConfig;
|
||||||
|
_newConfig = newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_plugin_update, null));
|
||||||
|
|
||||||
|
_buttonCancel1 = findViewById(R.id.button_cancel_1);
|
||||||
|
_buttonCancel2 = findViewById(R.id.button_cancel_2);
|
||||||
|
_buttonUpdate = findViewById(R.id.button_update);
|
||||||
|
|
||||||
|
_buttonOk = findViewById(R.id.button_ok);
|
||||||
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
|
_textPlugin = findViewById(R.id.text_plugin);
|
||||||
|
_textProgres = findViewById(R.id.text_progress);
|
||||||
|
_textError = findViewById(R.id.text_error);
|
||||||
|
_textResult = findViewById(R.id.text_result);
|
||||||
|
|
||||||
|
_uiChoiceTop = findViewById(R.id.dialog_ui_choice_top);
|
||||||
|
_uiProgressTop = findViewById(R.id.dialog_ui_progress_top);
|
||||||
|
_uiRiskTop = findViewById(R.id.dialog_ui_risk_top);
|
||||||
|
|
||||||
|
_uiChoiceBot = findViewById(R.id.dialog_ui_bottom_choice);
|
||||||
|
_uiResultBot = findViewById(R.id.dialog_ui_bottom_result);
|
||||||
|
_uiRiskBot = findViewById(R.id.dialog_ui_bottom_risk);
|
||||||
|
_uiProgressBot = findViewById(R.id.dialog_ui_bottom_progress);
|
||||||
|
|
||||||
|
_updateSpinner = findViewById(R.id.update_spinner);
|
||||||
|
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||||
|
|
||||||
|
_buttonCancel1.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
};
|
||||||
|
_buttonCancel2.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
};
|
||||||
|
_buttonUpdate.setOnClickListener {
|
||||||
|
if (_isUpdating)
|
||||||
|
return@setOnClickListener;
|
||||||
|
_isUpdating = true;
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
|
Glide.with(_iconPlugin)
|
||||||
|
.load(_oldConfig.absoluteIconUrl)
|
||||||
|
.fallback(R.drawable.ic_sources)
|
||||||
|
.into(_iconPlugin);
|
||||||
|
_textPlugin.text = _oldConfig.name;
|
||||||
|
|
||||||
|
val descriptor = StatePlugins.instance.getPlugin(_oldConfig.id);
|
||||||
|
if(descriptor != null) {
|
||||||
|
if(descriptor.appSettings.automaticUpdate) {
|
||||||
|
if (_isUpdating)
|
||||||
|
return;
|
||||||
|
_isUpdating = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
super.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update() {
|
||||||
|
_uiChoiceTop.visibility = View.GONE;
|
||||||
|
_uiRiskTop.visibility = View.GONE;
|
||||||
|
_uiChoiceBot.visibility = View.GONE;
|
||||||
|
_uiResultBot.visibility = View.GONE;
|
||||||
|
_uiRiskBot.visibility = View.GONE;
|
||||||
|
_uiProgressTop.visibility = View.VISIBLE;
|
||||||
|
_uiProgressBot.visibility = View.VISIBLE;
|
||||||
|
|
||||||
|
setCancelable(false);
|
||||||
|
setCanceledOnTouchOutside(false);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Keep screen on set import")
|
||||||
|
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
|
||||||
|
_updateSpinner.drawable?.assume<Animatable>()?.start();
|
||||||
|
|
||||||
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
scope?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val script = StatePlugins.instance.getScript(_oldConfig.id) ?: "";
|
||||||
|
|
||||||
|
val newScript = client.get(_newConfig.absoluteScriptUrl)?.body?.string();
|
||||||
|
if(newScript.isNullOrEmpty())
|
||||||
|
throw IllegalStateException("No script found");
|
||||||
|
|
||||||
|
if(_oldConfig.isLowRiskUpdate(script, _newConfig, newScript)){
|
||||||
|
|
||||||
|
StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, _newConfig, newScript,
|
||||||
|
{ text: String, progress: Double ->
|
||||||
|
_textProgres.setText(text);
|
||||||
|
},
|
||||||
|
{ ex ->
|
||||||
|
if(ex == null) {
|
||||||
|
StatePlugins.instance.clearUpdateAvailable(_newConfig);
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_check);
|
||||||
|
_textError.visibility = View.GONE;
|
||||||
|
_textResult.visibility = View.VISIBLE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||||
|
_textError.text = ex.message + "\n\nYou can retry inside the sources tab";
|
||||||
|
_textError.visibility = View.VISIBLE;
|
||||||
|
_textResult.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
_buttonOk.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiChoiceTop.visibility = View.VISIBLE;
|
||||||
|
_uiResultBot.visibility = View.VISIBLE;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update UI.", e)
|
||||||
|
} finally {
|
||||||
|
Logger.i(TAG, "Keep screen on unset update")
|
||||||
|
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
_buttonInstall.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
|
||||||
|
val intent = Intent(_context, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(_newConfig.sourceUrl)
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiRiskTop.visibility = View.VISIBLE;
|
||||||
|
_uiRiskBot.visibility = View.VISIBLE;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update UI.", e)
|
||||||
|
} finally {
|
||||||
|
Logger.i(TAG, "Keep screen on unset update")
|
||||||
|
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update.", e);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_buttonOk.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||||
|
_textResult.visibility = View.GONE;
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiChoiceTop.visibility = View.VISIBLE;
|
||||||
|
_uiResultBot.visibility = View.VISIBLE;
|
||||||
|
_textError.visibility = View.VISIBLE;
|
||||||
|
_textError.text = e.message + "\n\nYou can retry inside the sources tab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -316,7 +316,7 @@ class PackageHttp: V8Package {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/*private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
|
private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
|
||||||
Logger.v(TAG) {
|
Logger.v(TAG) {
|
||||||
val stringBuilder = StringBuilder();
|
val stringBuilder = StringBuilder();
|
||||||
stringBuilder.appendLine("HTTP request (useAuth = )");
|
stringBuilder.appendLine("HTTP request (useAuth = )");
|
||||||
|
@ -333,7 +333,7 @@ class PackageHttp: V8Package {
|
||||||
|
|
||||||
return@v stringBuilder.toString();
|
return@v stringBuilder.toString();
|
||||||
};
|
};
|
||||||
}*/
|
}
|
||||||
|
|
||||||
/*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
|
/*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
|
||||||
Logger.v(TAG) {
|
Logger.v(TAG) {
|
||||||
|
|
|
@ -152,7 +152,7 @@ class ChannelFragment : MainFragment() {
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.e(TAG, "Failed to load channel.", it);
|
Logger.e(TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() }, null, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
val tabs: TabLayout = findViewById(R.id.tabs);
|
val tabs: TabLayout = findViewById(R.id.tabs);
|
||||||
|
|
|
@ -99,7 +99,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load results.", it);
|
Logger.w(TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||||
|
|
|
@ -60,7 +60,7 @@ class CreatorSearchResultsFragment : MainFragment() {
|
||||||
.exception<ScriptCaptchaRequiredException> { }
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
});
|
}, null, fragment);
|
||||||
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,7 @@ class HistoryFragment : MainFragment() {
|
||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
});
|
}, null, fragment);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -126,10 +126,10 @@ class HomeFragment : MainFragment() {
|
||||||
Logger.w(TAG, "Failed to load channel.", it);
|
Logger.w(TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
||||||
loadResults()
|
loadResults()
|
||||||
}) {
|
}, {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
}, fragment);
|
||||||
};
|
};
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
|
|
|
@ -146,7 +146,7 @@ class PlaylistFragment : MainFragment() {
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load playlist.", it);
|
Logger.w(TAG, "Failed to load playlist.", it);
|
||||||
val c = context ?: return@exception;
|
val c = context ?: return@exception;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist);
|
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist, null, fragment);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ class PlaylistSearchResultsFragment : MainFragment() {
|
||||||
.success { loadedResult(it); }
|
.success { loadedResult(it); }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,7 @@ class PostDetailFragment : MainFragment {
|
||||||
.success { setPostDetails(it) }
|
.success { setPostDetails(it) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||||
|
|
|
@ -262,7 +262,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||||
if(it !is CancellationException)
|
if(it !is CancellationException)
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) }, null, fragment);
|
||||||
else {
|
else {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
@ -40,7 +40,7 @@ class SuggestionsFragment : MainFragment {
|
||||||
.success { suggestions -> updateSuggestions(suggestions, false) }
|
.success { suggestions -> updateSuggestions(suggestions, false) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load suggestions.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load suggestions.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() });
|
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() }, null, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(): super() {
|
constructor(): super() {
|
||||||
|
|
|
@ -2476,7 +2476,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||||
|
|
||||||
if (!nextVideo()) {
|
if (!nextVideo()) {
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo, null, fragment);
|
||||||
} else {
|
} else {
|
||||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString(
|
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString(
|
||||||
R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
||||||
|
@ -2512,7 +2512,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo, null, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
|
@ -2524,7 +2524,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment);
|
||||||
}
|
}
|
||||||
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||||
|
|
||||||
|
|
|
@ -571,18 +571,22 @@ class StateApp {
|
||||||
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
||||||
|
|
||||||
scopeOrNull?.launch(Dispatchers.IO) {
|
scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
val updateAvailable = StatePlatform.instance.checkForUpdates()
|
val updateAvailable = StatePlugins.instance.checkForUpdates()
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (updateAvailable.isNotEmpty()) {
|
if (updateAvailable.isNotEmpty()) {
|
||||||
UIDialogs.appToast(
|
UIDialogs.appToast(
|
||||||
ToastView.Toast(updateAvailable
|
ToastView.Toast(updateAvailable
|
||||||
.map { " - " + it.name }
|
.map { " - " + it.first.name }
|
||||||
.joinToString("\n"),
|
.joinToString("\n"),
|
||||||
true,
|
true,
|
||||||
null,
|
null,
|
||||||
"Plugin updates available"
|
"Plugin updates available"
|
||||||
));
|
));
|
||||||
|
|
||||||
|
for(update in updateAvailable)
|
||||||
|
if(StatePlatform.instance.isClientEnabled(update.first.id))
|
||||||
|
UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,6 @@ class StatePlatform {
|
||||||
private val _clientsLock = Object();
|
private val _clientsLock = Object();
|
||||||
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
||||||
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
|
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
|
||||||
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
|
||||||
|
|
||||||
//ClientPools are used to isolate plugin usage of certain components from others
|
//ClientPools are used to isolate plugin usage of certain components from others
|
||||||
//This prevents for example a background task like subscriptions from blocking a user from opening a video
|
//This prevents for example a background task like subscriptions from blocking a user from opening a video
|
||||||
|
@ -925,66 +924,7 @@ class StatePlatform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
|
||||||
val updatesAvailableMap = _updatesAvailableMap
|
|
||||||
synchronized(updatesAvailableMap) {
|
|
||||||
return updatesAvailableMap.contains(c.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
|
||||||
var configs = mutableListOf<SourcePluginConfig>()
|
|
||||||
val updatesAvailableFor = hashSetOf<String>()
|
|
||||||
for (availableClient in getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
|
||||||
if (availableClient !is JSClient) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkForUpdates(availableClient.config)) {
|
|
||||||
configs.add(availableClient.config);
|
|
||||||
updatesAvailableFor.add(availableClient.config.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_updatesAvailableMap = updatesAvailableFor
|
|
||||||
return@withContext configs;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
|
||||||
val updatesAvailableMap = _updatesAvailableMap
|
|
||||||
synchronized(updatesAvailableMap) {
|
|
||||||
updatesAvailableMap.remove(c.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun checkForUpdates(c: SourcePluginConfig): Boolean = withContext(Dispatchers.IO) {
|
|
||||||
val sourceUrl = c.sourceUrl ?: return@withContext false;
|
|
||||||
|
|
||||||
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
|
||||||
try {
|
|
||||||
val client = ManagedHttpClient();
|
|
||||||
val response = client.get(sourceUrl);
|
|
||||||
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
|
||||||
|
|
||||||
if (!response.isOk || response.body == null) {
|
|
||||||
return@withContext false;
|
|
||||||
}
|
|
||||||
|
|
||||||
val configJson = response.body.string();
|
|
||||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
|
||||||
|
|
||||||
val config = SourcePluginConfig.fromJson(configJson);
|
|
||||||
if (config.version <= c.version) {
|
|
||||||
return@withContext false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
|
||||||
return@withContext true;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to check for updates.", e);
|
|
||||||
return@withContext false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var _instance : StatePlatform? = null;
|
private var _instance : StatePlatform? = null;
|
||||||
|
|
|
@ -43,6 +43,7 @@ class StatePlugins {
|
||||||
private var _embeddedSourcesDefault: List<String>? = null
|
private var _embeddedSourcesDefault: List<String>? = null
|
||||||
private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
|
private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
|
||||||
|
|
||||||
|
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||||
|
|
||||||
fun getPluginIconOrNull(id: String): ImageVariable? {
|
fun getPluginIconOrNull(id: String): ImageVariable? {
|
||||||
if(iconsDir.hasIcon(id))
|
if(iconsDir.hasIcon(id))
|
||||||
|
@ -55,6 +56,70 @@ class StatePlugins {
|
||||||
.load();
|
.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun checkForUpdates(): List<Pair<SourcePluginConfig, SourcePluginConfig>> = withContext(Dispatchers.IO) {
|
||||||
|
var configs = mutableListOf<Pair<SourcePluginConfig, SourcePluginConfig>>()
|
||||||
|
val updatesAvailableFor = hashSetOf<String>()
|
||||||
|
for (availableClient in StatePlatform.instance.getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
||||||
|
if (availableClient !is JSClient) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val newConfig = checkForUpdates(availableClient.config);
|
||||||
|
if (newConfig != null) {
|
||||||
|
configs.add(Pair(availableClient.config, newConfig));
|
||||||
|
updatesAvailableFor.add(availableClient.config.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatesAvailableMap = updatesAvailableFor
|
||||||
|
return@withContext configs;
|
||||||
|
}
|
||||||
|
private suspend fun checkForUpdates(c: SourcePluginConfig): SourcePluginConfig? = withContext(Dispatchers.IO) {
|
||||||
|
val sourceUrl = c.sourceUrl ?: return@withContext null;
|
||||||
|
|
||||||
|
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
||||||
|
try {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val response = client.get(sourceUrl);
|
||||||
|
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
||||||
|
|
||||||
|
if (!response.isOk || response.body == null) {
|
||||||
|
return@withContext null;
|
||||||
|
}
|
||||||
|
|
||||||
|
val configJson = response.body.string();
|
||||||
|
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||||
|
|
||||||
|
val config = SourcePluginConfig.fromJson(configJson);
|
||||||
|
if (config.version <= c.version) {
|
||||||
|
return@withContext null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
||||||
|
return@withContext config;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to check for updates.", e);
|
||||||
|
return@withContext null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
||||||
|
val updatesAvailableMap = _updatesAvailableMap
|
||||||
|
synchronized(updatesAvailableMap) {
|
||||||
|
return updatesAvailableMap.contains(c.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
||||||
|
val updatesAvailableMap = _updatesAvailableMap
|
||||||
|
synchronized(updatesAvailableMap) {
|
||||||
|
updatesAvailableMap.remove(c.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun loginPlugin(context: Context, id: String, afterLogin: ()->Unit): Boolean {
|
fun loginPlugin(context: Context, id: String, afterLogin: ()->Unit): Boolean {
|
||||||
val descriptor = getPlugin(id) ?: return false;
|
val descriptor = getPlugin(id) ?: return false;
|
||||||
val config = descriptor.config;
|
val config = descriptor.config;
|
||||||
|
@ -353,6 +418,49 @@ class StatePlugins {
|
||||||
else verifyCanInstall();
|
else verifyCanInstall();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun installPluginBackground(context: Context, scope: CoroutineScope, config: SourcePluginConfig, script: String, onProgress: (text: String, progress: Double)->Unit, onConcluded: (ex: Throwable?)->Unit) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgress.invoke("Validating script", 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
val tempDescriptor = SourcePluginDescriptor(config);
|
||||||
|
val plugin = JSClient(context, tempDescriptor, null, script);
|
||||||
|
plugin.validate();
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgress.invoke("Downloading Icon", 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgress.invoke("Saving plugin", 0.75);
|
||||||
|
}
|
||||||
|
val iconResp = client.get(absIconUrl);
|
||||||
|
if(iconResp.isOk)
|
||||||
|
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
||||||
|
return@let null;
|
||||||
|
}
|
||||||
|
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
|
||||||
|
if(installEx != null)
|
||||||
|
throw installEx;
|
||||||
|
StatePlatform.instance.updateAvailableClients(context);
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgress.invoke("Finished", 1.0)
|
||||||
|
onConcluded.invoke(null);
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.e(TAG, ex.message ?: "null", ex);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onConcluded.invoke(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getPlugin(id: String): SourcePluginDescriptor? {
|
fun getPlugin(id: String): SourcePluginDescriptor? {
|
||||||
if(id == StateDeveloper.DEV_ID)
|
if(id == StateDeveloper.DEV_ID)
|
||||||
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
|
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
|
||||||
class DisabledSourceView : LinearLayout {
|
class DisabledSourceView : LinearLayout {
|
||||||
private val _root: LinearLayout;
|
private val _root: LinearLayout;
|
||||||
|
@ -37,7 +38,7 @@ class DisabledSourceView : LinearLayout {
|
||||||
|
|
||||||
_textSource.text = client.name;
|
_textSource.text = client.name;
|
||||||
|
|
||||||
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
if (client is JSClient && StatePlugins.instance.hasUpdateAvailable(client.config)) {
|
||||||
_textSourceSubtitle.text = context.getString(R.string.update_available_exclamation)
|
_textSourceSubtitle.text = context.getString(R.string.update_available_exclamation)
|
||||||
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
|
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
|
||||||
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
|
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
|
||||||
class EnabledSourceViewHolder : ViewHolder {
|
class EnabledSourceViewHolder : ViewHolder {
|
||||||
private val _imageSource: ImageView;
|
private val _imageSource: ImageView;
|
||||||
|
@ -61,7 +62,7 @@ class EnabledSourceViewHolder : ViewHolder {
|
||||||
|
|
||||||
_textSource.text = client.name
|
_textSource.text = client.name
|
||||||
|
|
||||||
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
if (client is JSClient && StatePlugins.instance.hasUpdateAvailable(client.config)) {
|
||||||
_textSourceSubtitle.text = itemView.context.getString(R.string.update_available_exclamation)
|
_textSourceSubtitle.text = itemView.context.getString(R.string.update_available_exclamation)
|
||||||
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
|
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
|
||||||
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
|
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
|
||||||
|
|
315
app/src/main/res/layout/dialog_plugin_update.xml
Normal file
315
app/src/main/res/layout/dialog_plugin_update.xml
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@color/gray_1d">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingTop="30dp"
|
||||||
|
android:paddingBottom="30dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/dialog_ui_choice_top"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="visible">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon_plugin"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_sources" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/dialog_ui_risk_top"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_warning_yellow" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/dialog_ui_progress_top"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/update_spinner"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:srcCompat="@drawable/ic_update_animated"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Plugin Update"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_extra_light"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_plugin"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Some Plugin Name"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/dialog_ui_bottom_choice"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="visible"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="A new update is available.\nWould you like to update this plugin?"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="Updates may be critical to functionality"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/pastel_red"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="28dp">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_cancel_1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/cancel"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:background="@color/transparent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_update"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/background_button_primary"
|
||||||
|
android:layout_marginEnd="28dp"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Update"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:paddingStart="28dp"
|
||||||
|
android:paddingEnd="28dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/dialog_ui_bottom_progress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:id="@+id/text_progress"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="This plugin has modified its permissions"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/dialog_ui_bottom_risk"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="This plugin has modified its permissions"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="Make sure you read the installation screen"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/pastel_red"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="28dp">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_cancel_2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/cancel"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:background="@color/transparent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_install"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/background_button_primary"
|
||||||
|
android:layout_marginEnd="28dp"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Reinstall"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:paddingStart="28dp"
|
||||||
|
android:paddingEnd="28dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/dialog_ui_bottom_result"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_error"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text=""
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/pastel_red"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_result"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="Succesfully updated plugin."
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:layout_marginEnd="30dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="28dp">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_ok"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/background_button_primary"
|
||||||
|
android:layout_marginEnd="28dp"
|
||||||
|
android:clickable="true">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Ok"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:paddingStart="28dp"
|
||||||
|
android:paddingEnd="28dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
|
@ -489,6 +489,8 @@
|
||||||
<string name="visibility">Visibility</string>
|
<string name="visibility">Visibility</string>
|
||||||
<string name="check_for_updates_setting">Check for updates</string>
|
<string name="check_for_updates_setting">Check for updates</string>
|
||||||
<string name="check_for_updates_setting_description">If a plugin should be checked for updates on startup</string>
|
<string name="check_for_updates_setting_description">If a plugin should be checked for updates on startup</string>
|
||||||
|
<string name="automatic_update_setting">Automatic Update</string>
|
||||||
|
<string name="automatic_update_setting_description">Update automatically on boot if no permissions changed and plugin is enabled</string>
|
||||||
<string name="allow_developer_submit">Allow Developer Submissions</string>
|
<string name="allow_developer_submit">Allow Developer Submissions</string>
|
||||||
<string name="allow_developer_submit_description">Allows the developer to send data to their server, be careful as this might include sensitive data.</string>
|
<string name="allow_developer_submit_description">Allows the developer to send data to their server, be careful as this might include sensitive data.</string>
|
||||||
<string name="allow_developer_submit_warning">Make sure you trust the developer. They may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.</string>
|
<string name="allow_developer_submit_warning">Make sure you trust the developer. They may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.</string>
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Testing",
|
|
||||||
"description": "Just for testing.",
|
|
||||||
"author": "FUTO",
|
|
||||||
"authorUrl": "https://futo.org",
|
|
||||||
|
|
||||||
"platformUrl": "https://odysee.com",
|
|
||||||
"sourceUrl": "https://plugins.grayjay.app/Test/TestConfig.json",
|
|
||||||
"repositoryUrl": "https://futo.org",
|
|
||||||
"scriptUrl": "./TestScript.js",
|
|
||||||
"version": 31,
|
|
||||||
|
|
||||||
"iconUrl": "./odysee.png",
|
|
||||||
"id": "1c05bfc3-08b9-42d0-93d3-6d52e0fd34d8",
|
|
||||||
|
|
||||||
"scriptSignature": "",
|
|
||||||
"scriptPublicKey": "",
|
|
||||||
"packages": ["Http"],
|
|
||||||
|
|
||||||
"allowEval": false,
|
|
||||||
"allowUrls": [],
|
|
||||||
|
|
||||||
"supportedClaimTypes": []
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
var config = {};
|
|
||||||
|
|
||||||
//Source Methods
|
|
||||||
source.enable = function(conf){
|
|
||||||
config = conf ?? {};
|
|
||||||
//log(config);
|
|
||||||
}
|
|
||||||
source.getHome = function() {
|
|
||||||
return new ContentPager([
|
|
||||||
source.getContentDetails("whatever")
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
//Video
|
|
||||||
source.isContentDetailsUrl = function(url) {
|
|
||||||
return REGEX_DETAILS_URL.test(url)
|
|
||||||
};
|
|
||||||
source.getContentDetails = function(url) {
|
|
||||||
return new PlatformVideoDetails({
|
|
||||||
id: new PlatformID("Test", "Something", config.id),
|
|
||||||
name: "Test Video",
|
|
||||||
thumbnails: new Thumbnails([]),
|
|
||||||
author: new PlatformAuthorLink(new PlatformID("Test", "TestID", config.id),
|
|
||||||
"TestAuthor",
|
|
||||||
"None",
|
|
||||||
""),
|
|
||||||
datetime: parseInt(new Date().getTime() / 1000),
|
|
||||||
duration: 0,
|
|
||||||
viewCount: 0,
|
|
||||||
url: "",
|
|
||||||
isLive: false,
|
|
||||||
description: "",
|
|
||||||
rating: new RatingLikes(0),
|
|
||||||
video: new VideoSourceDescriptor([
|
|
||||||
new HLSSource({
|
|
||||||
name: "HLS",
|
|
||||||
url: "",
|
|
||||||
duration: 0,
|
|
||||||
priority: true
|
|
||||||
})
|
|
||||||
])
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
log("LOADED");
|
|
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
|
@ -1 +1 @@
|
||||||
Subproject commit cac27408440f5586c1c68d846456792041403d35
|
Subproject commit d1058f0b6ccf8cbebe4eed2afba145899e6dba00
|
Loading…
Add table
Add a link
Reference in a new issue