mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
e599729ba1
17 changed files with 182 additions and 133 deletions
|
@ -685,7 +685,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||
fun manualCheck() {
|
||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
StateUpdate.instance.checkForUpdates(it, true);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(it, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
|
|
|
@ -216,8 +216,10 @@ class AddSourceActivity : AppCompatActivity() {
|
|||
|
||||
fun install(config: SourcePluginConfig, script: String) {
|
||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||
if(it)
|
||||
if(it) {
|
||||
StatePlatform.instance.clearUpdateAvailable(config)
|
||||
backToSources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,11 @@ import android.widget.TextView
|
|||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.logging.LogLevel
|
||||
import com.futo.platformplayer.logging.Logging
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
@ -454,6 +454,7 @@ class SourceDetailFragment : MainFragment() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fun checkForUpdatesSource() {
|
||||
val c = _config ?: return;
|
||||
val sourceUrl = c.sourceUrl ?: return;
|
||||
|
|
|
@ -460,7 +460,9 @@ class StateApp {
|
|||
|
||||
//Foreground download
|
||||
autoUpdateEnabled -> {
|
||||
StateUpdate.instance.checkForUpdates(context, false);
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(context, false)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
@ -558,6 +560,23 @@ class StateApp {
|
|||
|
||||
if(StateHistory.instance.shouldMigrateLegacyHistory())
|
||||
StateHistory.instance.migrateLegacyHistory();
|
||||
|
||||
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
||||
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val updateAvailableCount = StatePlatform.instance.checkForUpdates()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (updateAvailableCount > 0) {
|
||||
StateAnnouncement.instance.registerAnnouncement(
|
||||
"plugin-update",
|
||||
"Plugin updates available",
|
||||
"There are $updateAvailableCount plugin updates available.",
|
||||
AnnouncementType.SESSION_RECURRING
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.collection.LruCache
|
|||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
||||
|
@ -78,6 +79,7 @@ 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
|
||||
|
@ -932,6 +934,67 @@ class StatePlatform {
|
|||
}
|
||||
}
|
||||
|
||||
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
return updatesAvailableMap.contains(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(): Int = withContext(Dispatchers.IO) {
|
||||
var updateAvailableCount = 0
|
||||
val updatesAvailableFor = hashSetOf<String>()
|
||||
for (availableClient in getAvailableClients()) {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (checkForUpdates(availableClient.config)) {
|
||||
updateAvailableCount++
|
||||
updatesAvailableFor.add(availableClient.config.id)
|
||||
}
|
||||
}
|
||||
|
||||
_updatesAvailableMap = updatesAvailableFor
|
||||
return@withContext updateAvailableCount
|
||||
}
|
||||
|
||||
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;
|
||||
val instance : StatePlatform
|
||||
|
|
|
@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
|||
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
|
@ -467,7 +466,6 @@ class StatePlugins {
|
|||
_plugins.save(descriptor);
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
private data class PluginConfig(
|
||||
val SOURCES_EMBEDDED: Map<String, String>,
|
||||
|
|
|
@ -2,15 +2,15 @@ package com.futo.platformplayer.states
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.copyToOutputStream
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
|
@ -155,47 +155,45 @@ class StateUpdate {
|
|||
}
|
||||
}
|
||||
|
||||
fun checkForUpdates(context: Context, showUpToDateToast: Boolean) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val latestVersion = downloadVersionCode(client);
|
||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val latestVersion = downloadVersionCode(client);
|
||||
|
||||
if (latestVersion != null) {
|
||||
val currentVersion = BuildConfig.VERSION_CODE;
|
||||
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
||||
if (latestVersion != null) {
|
||||
val currentVersion = BuildConfig.VERSION_CODE;
|
||||
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
||||
|
||||
if (latestVersion > currentVersion) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.toast(context, "Failed to show update dialog");
|
||||
Logger.w(TAG, "Error occurred in update dialog.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (showUpToDateToast) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Already on latest version");
|
||||
}
|
||||
if (latestVersion > currentVersion) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.toast(context, "Failed to show update dialog");
|
||||
Logger.w(TAG, "Error occurred in update dialog.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to retrieve version");
|
||||
if (showUpToDateToast) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Already on latest version");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to check for updates.", e);
|
||||
} else {
|
||||
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to check for updates");
|
||||
UIDialogs.toast(context, "Failed to retrieve version");
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to check for updates.", e);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to check for updates");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadApkToFile(client: ManagedHttpClient, destinationFile: File, isCancelled: (() -> Boolean)? = null) {
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
|
||||
class DisabledSourceAdapter : RecyclerView.Adapter<DisabledSourceViewHolder> {
|
||||
private val _sources: MutableList<IPlatformClient>;
|
||||
|
||||
var onClick = Event1<IPlatformClient>();
|
||||
var onAdd = Event1<IPlatformClient>();
|
||||
|
||||
constructor(sources: MutableList<IPlatformClient>) : super() {
|
||||
_sources = sources;
|
||||
}
|
||||
|
||||
override fun getItemCount() = _sources.size
|
||||
|
||||
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): DisabledSourceViewHolder {
|
||||
val holder = DisabledSourceViewHolder(viewGroup);
|
||||
holder.onAdd.subscribe {
|
||||
val source = holder.source;
|
||||
if (source != null) {
|
||||
onAdd.emit(source);
|
||||
}
|
||||
}
|
||||
holder.onClick.subscribe {
|
||||
val source = holder.source;
|
||||
if (source != null) {
|
||||
onClick.emit(source);
|
||||
}
|
||||
};
|
||||
return holder;
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: DisabledSourceViewHolder, position: Int) {
|
||||
viewHolder.bind(_sources[position])
|
||||
}
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
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
|
||||
|
||||
class DisabledSourceView : LinearLayout {
|
||||
private val _root: LinearLayout;
|
||||
|
@ -38,7 +36,16 @@ class DisabledSourceView : LinearLayout {
|
|||
client.icon?.setImageView(_imageSource);
|
||||
|
||||
_textSource.text = client.name;
|
||||
_textSourceSubtitle.text = context.getString(R.string.tap_to_open);
|
||||
|
||||
if (client is JSClient && StatePlatform.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)
|
||||
} else {
|
||||
_textSourceSubtitle.text = context.getString(R.string.tap_to_open)
|
||||
_textSourceSubtitle.setTextColor(context.getColor(R.color.gray_ac))
|
||||
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_extra_light)
|
||||
}
|
||||
|
||||
_buttonAdd.setOnClickListener { onAdd.emit(source) }
|
||||
_root.setOnClickListener { onClick.emit(); };
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
|
||||
class DisabledSourceViewHolder : ViewHolder {
|
||||
private val _imageSource: ImageView;
|
||||
private val _textSource: TextView;
|
||||
private val _textSourceSubtitle: TextView;
|
||||
|
||||
private val _buttonAdd: LinearLayout;
|
||||
|
||||
var onClick = Event0();
|
||||
var onAdd = Event0();
|
||||
var source: IPlatformClient? = null
|
||||
private set
|
||||
|
||||
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_source_disabled, viewGroup, false)) {
|
||||
_imageSource = itemView.findViewById(R.id.image_source);
|
||||
_textSource = itemView.findViewById(R.id.text_source);
|
||||
_textSourceSubtitle = itemView.findViewById(R.id.text_source_subtitle);
|
||||
_buttonAdd = itemView.findViewById(R.id.button_add);
|
||||
|
||||
val root = itemView.findViewById<LinearLayout>(R.id.root);
|
||||
_buttonAdd.setOnClickListener { onAdd.emit() }
|
||||
root.setOnClickListener { onClick.emit(); };
|
||||
}
|
||||
|
||||
fun bind(client: IPlatformClient) {
|
||||
client.icon?.setImageView(_imageSource);
|
||||
|
||||
_textSource.text = client.name;
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
|
||||
source = client;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,9 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
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
|
||||
|
||||
class EnabledSourceViewHolder : ViewHolder {
|
||||
private val _imageSource: ImageView;
|
||||
|
@ -57,8 +59,18 @@ class EnabledSourceViewHolder : ViewHolder {
|
|||
fun bind(client: IPlatformClient) {
|
||||
client.icon?.setImageView(_imageSource);
|
||||
|
||||
_textSource.text = client.name;
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
|
||||
_textSource.text = client.name
|
||||
|
||||
if (client is JSClient && StatePlatform.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)
|
||||
} else {
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open)
|
||||
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.gray_ac))
|
||||
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_extra_light)
|
||||
}
|
||||
|
||||
source = client
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ class SourceHeaderView : LinearLayout {
|
|||
private val _sourcePlatformUrl: TextView;
|
||||
private val _sourceRepositoryUrl: TextView;
|
||||
private val _sourceScriptUrl: TextView;
|
||||
private val _sourceScriptConfig: TextView;
|
||||
private val _sourceSignature: TextView;
|
||||
|
||||
private val _sourcePlatformUrlContainer: LinearLayout;
|
||||
|
@ -45,6 +46,7 @@ class SourceHeaderView : LinearLayout {
|
|||
_sourcePlatformUrl = findViewById(R.id.source_platform);
|
||||
_sourcePlatformUrlContainer = findViewById(R.id.source_platform_container);
|
||||
_sourceScriptUrl = findViewById(R.id.source_script);
|
||||
_sourceScriptConfig = findViewById(R.id.source_config);
|
||||
_sourceSignature = findViewById(R.id.source_signature);
|
||||
|
||||
_sourceBy.setOnClickListener {
|
||||
|
@ -59,6 +61,10 @@ class SourceHeaderView : LinearLayout {
|
|||
if(!_config?.absoluteScriptUrl.isNullOrEmpty())
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.absoluteScriptUrl)));
|
||||
};
|
||||
_sourceScriptConfig.setOnClickListener {
|
||||
if(!_config?.sourceUrl.isNullOrEmpty())
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.sourceUrl)));
|
||||
}
|
||||
_sourcePlatformUrl.setOnClickListener {
|
||||
if(!_config?.platformUrl.isNullOrEmpty())
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(_config?.platformUrl)));
|
||||
|
@ -82,6 +88,7 @@ class SourceHeaderView : LinearLayout {
|
|||
_sourceVersion.text = config.version.toString();
|
||||
_sourceScriptUrl.text = config.absoluteScriptUrl;
|
||||
_sourceRepositoryUrl.text = config.repositoryUrl;
|
||||
_sourceScriptConfig.text = config.sourceUrl
|
||||
_sourceAuthorID.text = "";
|
||||
|
||||
_sourcePlatformUrl.text = config.platformUrl ?: "";
|
||||
|
|
|
@ -170,6 +170,28 @@
|
|||
tools:text="https://some.repository.url/whatever/someScript.js" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="10dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/config_url" />
|
||||
<TextView
|
||||
android:id="@+id/source_config"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
tools:text="https://some.repository.url/whatever/someScript.js" />
|
||||
</LinearLayout>
|
||||
|
||||
<!--Script Url-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -113,6 +113,7 @@
|
|||
<string name="platform_url">Platform URL</string>
|
||||
<string name="repository_url">Repository URL</string>
|
||||
<string name="script_url">Script URL</string>
|
||||
<string name="config_url">Config URL</string>
|
||||
<string name="source_permissions_explanation">These are the permissions the plugin requires to function</string>
|
||||
<string name="source_explain_eval_access">The plugin will have access to eval capacity</string>
|
||||
<string name="source_explain_script_url">The plugin will have access to the following domains</string>
|
||||
|
@ -651,6 +652,7 @@
|
|||
<string name="please_use_at_least_3_characters">Please use at least 3 characters</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">Are you sure you want to delete this video?</string>
|
||||
<string name="tap_to_open">Tap to open</string>
|
||||
<string name="update_available_exclamation">Update available!</string>
|
||||
<string name="watching">watching</string>
|
||||
<string name="available_in">available in</string>
|
||||
<string name="seconds">seconds</string>
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit a21ad56829b0f0b45bbd677979f3d260e13abb34
|
||||
Subproject commit 206d996801b9734cae13093859375c70be980a8d
|
|
@ -1 +1 @@
|
|||
Subproject commit a21ad56829b0f0b45bbd677979f3d260e13abb34
|
||||
Subproject commit 206d996801b9734cae13093859375c70be980a8d
|
Loading…
Add table
Reference in a new issue