diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
index ec6664622a5f52068d0f95be15d892a5137cffc2..c9169091ef41388f7dda2f950a136fcbe84422c7 100644
--- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
+++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
@@ -18,6 +18,7 @@ import android.widget.Toast
 import androidx.core.content.ContextCompat
 import com.futo.platformplayer.activities.MainActivity
 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.dialogs.AutoUpdateDialog
 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.ImportOptionsDialog
 import com.futo.platformplayer.dialogs.MigrateDialog
+import com.futo.platformplayer.dialogs.PluginUpdateDialog
 import com.futo.platformplayer.dialogs.ProgressDialog
 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.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 com.futo.platformplayer.views.ToastView
 import kotlinx.coroutines.CoroutineScope
@@ -184,6 +190,14 @@ class UIDialogs {
             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) {
             val builder = AlertDialog.Builder(context);
             val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
@@ -269,22 +283,48 @@ class UIDialogs {
                 }, 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)
                 "\nPlugin [${ex.config.name}]" else "";
-            showDialog(context,
-                R.drawable.ic_error_pred,
-                "${msg}${pluginInfo}",
-                (if(ex != null ) "${ex.message}" else ""),
-                if(ex is PluginException) ex.code else null,
-                0,
-                UIDialogs.Action(context.getString(R.string.retry), {
-                    retryAction?.invoke();
-                }, UIDialogs.ActionStyle.PRIMARY),
-                UIDialogs.Action(context.getString(R.string.close), {
-                    closeAction?.invoke()
-                }, UIDialogs.ActionStyle.NONE)
-            );
+
+            var exMsg = if(ex != null ) "${ex.message}" else "";
+            if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
+                exMsg += "\n\nAn update is available"
+
+            if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
+                showDialog(context,
+                    R.drawable.ic_error_pred,
+                    "${msg}${pluginInfo}",
+                    exMsg,
+                    if(ex is PluginException) ex.code else null,
+                    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)) {
diff --git a/app/src/main/java/com/futo/platformplayer/activities/AddSourceActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/AddSourceActivity.kt
index 85873d28a7d518fc9407b95e2547a3370d8a37d7..81be84eaf485d627eb5f9883f6f36d9ab820df71 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/AddSourceActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/AddSourceActivity.kt
@@ -224,7 +224,7 @@ class AddSourceActivity : AppCompatActivity() {
         val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
         StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
             if(it) {
-                StatePlatform.instance.clearUpdateAvailable(config)
+                StatePlugins.instance.clearUpdateAvailable(config)
                 if(isNew)
                     lifecycleScope.launch {
                         StatePlatform.instance.enableClient(listOf(config.id));
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
index 64ceb31aa6e8e5d1c8c106bc1721278ad486732e..0a3498bb956b72b0c512fc8b43e93e31f6fe70ae 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt
@@ -46,6 +46,7 @@ import com.futo.platformplayer.constructs.Event2
 import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.engine.exceptions.PluginEngineException
 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.ScriptValidationException
 import com.futo.platformplayer.logging.Logger
@@ -56,6 +57,7 @@ import com.futo.platformplayer.states.StatePlatform
 import com.futo.platformplayer.states.StatePlugins
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
+import java.lang.Exception
 import java.time.OffsetDateTime
 import kotlin.reflect.full.findAnnotations
 import kotlin.reflect.jvm.kotlinFunction
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
index 30196913c5d5f264d1d0363ab3c941cefdaddee3..e279fc0f041707b475374c9cede7621c54fe1c40 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt
@@ -80,6 +80,41 @@ class SourcePluginConfig(
         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>> {
         val list = mutableListOf<Pair<String,String>>();
 
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt
index bea18399fd1899d7202278d083cdc0f9de92cc99..add531314368178dd963d7949cbde29210990613 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginDescriptor.kt
@@ -91,8 +91,10 @@ class SourcePluginDescriptor {
     @Serializable
     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;
+        @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)
         var tabEnabled = TabEnabled();
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/PluginUpdateDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/PluginUpdateDialog.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b7109a815100ada164d156a80112d501676d76bb
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/PluginUpdateDialog.kt
@@ -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"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
index 937e2d23bcb05272e4c9b71886f38fa7bdd54eab..b1b7f4609b7ac32eacb465c51513161d3ce69c3a 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt
@@ -316,7 +316,7 @@ class PackageHttp: V8Package {
             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) {
                 val stringBuilder = StringBuilder();
                 stringBuilder.appendLine("HTTP request (useAuth = )");
@@ -333,7 +333,7 @@ class PackageHttp: V8Package {
 
                 return@v stringBuilder.toString();
             };
-        }*/
+        }
 
         /*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
             Logger.v(TAG) {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
index 602620ad892edb09d8f291c719cd49fd35bab974..3359119fa6ffb614afc0bf1c5388293cc15ec847 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt
@@ -152,7 +152,7 @@ class ChannelFragment : MainFragment() {
                 }
                 .exception<Throwable> {
                     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);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
index 828316869af96d8299f3854edc8ed53e368b4e85..f7a347771d76648ebe12174060e0224d286ba4c0 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentSearchResultsFragment.kt
@@ -99,7 +99,7 @@ class ContentSearchResultsFragment : MainFragment() {
             .success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> {  }
             .exception<Throwable> {
                 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);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorSearchResultsFragment.kt
index eac18ba9b5116f07356f1cec8a915926b61ab6cc..a7570846ac5b0ddd85e75433e818e7f27ee2539b 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorSearchResultsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorSearchResultsFragment.kt
@@ -60,7 +60,7 @@ class CreatorSearchResultsFragment : MainFragment() {
                 .exception<ScriptCaptchaRequiredException> {  }
                 .exception<Throwable> {
                     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);
                 }
         }
 
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
index 78459af492ea8ffcbb179fb6bc615b2fb66bf59d..7f0e1ccc976ba7d285fa88419e1ca8b17dc10133 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
@@ -144,7 +144,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
             Logger.w(TAG, "Failed to load next page.", it);
             UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
                 loadNextPage();
-            });
+            }, null, fragment);
             //UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
         };
 
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt
index fcbec07c12c629d45a42d58c7173e88db1db6f8f..2da46620f9175958a514046a4f4403bbb004e0a1 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt
@@ -174,7 +174,7 @@ class HistoryFragment : MainFragment() {
                 Logger.w(TAG, "Failed to load next page.", it);
                 UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
                     loadNextPage();
-                });
+                }, null, fragment);
             };
         }
 
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt
index 3e60c4ebb95a00507881080a46591892c88d6297..d09947837f4685eac6dc2d80c50161e1f867c12c 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt
@@ -126,10 +126,10 @@ class HomeFragment : MainFragment() {
                 Logger.w(TAG, "Failed to load channel.", it);
                 UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
                     loadResults()
-                }) {
+                }, {
                     finishRefreshLayoutLoader();
                     setLoading(false);
-                };
+                }, fragment);
             };
 
             setPreviewsEnabled(Settings.instance.home.previewFeedItems);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
index ca7a57fe2330f70e4ab8b92c92ea378b9d2e977a..d938e97033716bbdc247122f7d95ad6552c37946 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt
@@ -146,7 +146,7 @@ class PlaylistFragment : MainFragment() {
                 .exception<Throwable> {
                     Logger.w(TAG, "Failed to load playlist.", it);
                     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);
                 };
         }
 
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistSearchResultsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistSearchResultsFragment.kt
index ec01bb80f17d25c8e118138905577a532871171c..7900ff16dfb17aef9cc839ea86958739d70684e3 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistSearchResultsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistSearchResultsFragment.kt
@@ -69,7 +69,7 @@ class PlaylistSearchResultsFragment : MainFragment() {
                 .success { loadedResult(it); }
                 .exception<Throwable> {
                     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);
                 }
         }
 
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
index f149b675ba19a755cdbe81a96cdc2953fb48ddfc..22ad5e93a773753e0aba1d3ab13808a7e2b2534d 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
@@ -162,7 +162,7 @@ class PostDetailFragment : MainFragment {
             .success { setPostDetails(it) }
             .exception<Throwable> {
                 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 };
 
         private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
index 3099638f6fbbbb556bd97d4e5a936a445dfb440d..03d9a6269731ccafb36573bf1dcfe15bacbb5ea0 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
@@ -262,7 +262,7 @@ class SubscriptionsFeedFragment : MainFragment() {
             .exception<Throwable> {
                 Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
                 if(it !is CancellationException)
-                    UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
+                    UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) }, null, fragment);
                 else {
                     finishRefreshLayoutLoader();
                     setLoading(false);
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt
index e5499062cb36b13ee456128eea5c8b7e07c14c21..2f7254dc9e072c51b008dddceb54967698d8b342 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SuggestionsFragment.kt
@@ -40,7 +40,7 @@ class SuggestionsFragment : MainFragment {
     .success { suggestions -> updateSuggestions(suggestions, false) }
     .exception<Throwable> {
         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() {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index 9017ebc49a2ce122a8b5f74a99a9a91ff0bbd1da..6d0901e3333a9bba19ee3215cead8313b57ade78 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -2476,7 +2476,7 @@ class VideoDetailView : ConstraintLayout {
             Logger.w(TAG, "exception<ScriptImplementationException>", it)
 
             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 {
                 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)
@@ -2512,7 +2512,7 @@ class VideoDetailView : ConstraintLayout {
                 _retryJob = null;
                 _liveTryJob?.cancel();
                 _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> {
@@ -2524,7 +2524,7 @@ class VideoDetailView : ConstraintLayout {
                 _retryJob = null;
                 _liveTryJob?.cancel();
                 _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});
 
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
index 1142310c542c649a6258e6e3b1e1d29cce3181ed..7fd26d6384a06929ef13695f546d0149fa64ee99 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -571,18 +571,22 @@ class StateApp {
         StateAnnouncement.instance.deleteAnnouncement("plugin-update")
 
         scopeOrNull?.launch(Dispatchers.IO) {
-            val updateAvailable = StatePlatform.instance.checkForUpdates()
+            val updateAvailable = StatePlugins.instance.checkForUpdates()
 
             withContext(Dispatchers.Main) {
                 if (updateAvailable.isNotEmpty()) {
                     UIDialogs.appToast(
                         ToastView.Toast(updateAvailable
-                            .map { " - " + it.name }
+                            .map { " - " + it.first.name }
                             .joinToString("\n"),
                             true,
                             null,
                             "Plugin updates available"
                         ));
+
+                    for(update in updateAvailable)
+                        if(StatePlatform.instance.isClientEnabled(update.first.id))
+                            UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
                 }
             }
         }
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
index 1a3132a302fdcb70502db9a64abf383fbd386d55..0b03bab16b715f18e7369acadc0d5d52d09a0086 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
@@ -80,7 +80,6 @@ class StatePlatform {
     private val _clientsLock = Object();
     private val _availableClients : 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
     //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 {
         private var _instance : StatePlatform? = null;
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
index d0c2d8bebd00aee396a2c649393fcfe4033b6f2f..420a3721efad24bee6e03d55736262a6b72a5e75 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt
@@ -43,6 +43,7 @@ class StatePlugins {
     private var _embeddedSourcesDefault: List<String>? = null
     private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
 
+    private var _updatesAvailableMap: HashSet<String> = hashSetOf();
 
     fun getPluginIconOrNull(id: String): ImageVariable? {
         if(iconsDir.hasIcon(id))
@@ -55,6 +56,70 @@ class StatePlugins {
             .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 {
         val descriptor = getPlugin(id) ?: return false;
         val config = descriptor.config;
@@ -353,6 +418,49 @@ class StatePlugins {
         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? {
         if(id == StateDeveloper.DEV_ID)
             throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/DisabledSourceView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/DisabledSourceView.kt
index a7194ae73eacfbbc5bdb7aeb20ea21811645d502..b2768d5e6fc2f57739ef2a2343ad4ce80f3ece1d 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/DisabledSourceView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/DisabledSourceView.kt
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.constructs.Event0
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.states.StatePlatform
+import com.futo.platformplayer.states.StatePlugins
 
 class DisabledSourceView : LinearLayout {
     private val _root: LinearLayout;
@@ -37,7 +38,7 @@ class DisabledSourceView : LinearLayout {
 
         _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.setTextColor(context.getColor(R.color.light_blue_400))
             _textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/EnabledSourceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/EnabledSourceViewHolder.kt
index 9eed743bde8ff14cb4c566b821cabce4d2e322c6..d2cc258bbdf9e266d30f41a1d3786469bfbd06f2 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/EnabledSourceViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/EnabledSourceViewHolder.kt
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
 import com.futo.platformplayer.api.media.platforms.js.JSClient
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.states.StatePlatform
+import com.futo.platformplayer.states.StatePlugins
 
 class EnabledSourceViewHolder : ViewHolder {
     private val _imageSource: ImageView;
@@ -61,7 +62,7 @@ class EnabledSourceViewHolder : ViewHolder {
 
         _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.setTextColor(itemView.context.getColor(R.color.light_blue_400))
             _textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
diff --git a/app/src/main/res/layout/dialog_plugin_update.xml b/app/src/main/res/layout/dialog_plugin_update.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7c89e8bc05d4d69a8f5c6d9b551b355ff9cac7dd
--- /dev/null
+++ b/app/src/main/res/layout/dialog_plugin_update.xml
@@ -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>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index caf59143862163dacb51d5463757581d96646e26..786fbd9941d8cb91fd1a4e44c6107f45295a9287 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -489,6 +489,8 @@
     <string name="visibility">Visibility</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="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_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>
diff --git a/app/src/unstable/assets/sources/test/TestConfig.json b/app/src/unstable/assets/sources/test/TestConfig.json
deleted file mode 100644
index 86eed6ee1d7d7589c9e6c316c5b279a1eacbb9d1..0000000000000000000000000000000000000000
--- a/app/src/unstable/assets/sources/test/TestConfig.json
+++ /dev/null
@@ -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": []
-}
diff --git a/app/src/unstable/assets/sources/test/TestScript.js b/app/src/unstable/assets/sources/test/TestScript.js
deleted file mode 100644
index 45c47d8f076d1d29931c8149f2a87e4c2723219b..0000000000000000000000000000000000000000
--- a/app/src/unstable/assets/sources/test/TestScript.js
+++ /dev/null
@@ -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");
\ No newline at end of file
diff --git a/app/src/unstable/assets/sources/test/odysee.png b/app/src/unstable/assets/sources/test/odysee.png
deleted file mode 100644
index 472960d00a49401c1d97199ff6fa90cc337bd9bc..0000000000000000000000000000000000000000
Binary files a/app/src/unstable/assets/sources/test/odysee.png and /dev/null differ
diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube
index cac27408440f5586c1c68d846456792041403d35..d1058f0b6ccf8cbebe4eed2afba145899e6dba00 160000
--- a/app/src/unstable/assets/sources/youtube
+++ b/app/src/unstable/assets/sources/youtube
@@ -1 +1 @@
-Subproject commit cac27408440f5586c1c68d846456792041403d35
+Subproject commit d1058f0b6ccf8cbebe4eed2afba145899e6dba00