Remove built-in official extension repo support

This commit is contained in:
arkon 2024-01-07 22:22:32 -05:00
parent c91ec9a33b
commit bf737cf95c
14 changed files with 50 additions and 129 deletions

View file

@ -7,7 +7,7 @@ class CreateSourceRepo(private val preferences: SourcePreferences) {
fun await(name: String): Result { fun await(name: String): Result {
// Do not allow invalid formats // Do not allow invalid formats
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) { if (!name.matches(repoRegex)) {
return Result.InvalidUrl return Result.InvalidUrl
} }
@ -22,5 +22,4 @@ class CreateSourceRepo(private val preferences: SourcePreferences) {
} }
} }
const val OFFICIAL_REPO_BASE_URL = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo"
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex() private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()

View file

@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -67,13 +67,23 @@ fun ExtensionDetailsScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
state: ExtensionDetailsScreenModel.State, state: ExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickEnableAll: () -> Unit, onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit, onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit, onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
) { ) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
regex.find(state.extension?.repoUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
?: state.extension?.repoUrl
}
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( AppBar(
@ -83,12 +93,14 @@ fun ExtensionDetailsScreen(
AppBarActions( AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder() actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply { .apply {
if (state.extension?.isUnofficial == false) { if (url != null) {
add( add(
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.whats_new), title = stringResource(MR.strings.action_open_repo),
icon = Icons.Outlined.History, icon = Icons.AutoMirrored.Outlined.Launch,
onClick = onClickWhatsNew, onClick = {
uriHandler.openUri(url)
},
), ),
) )
} }
@ -150,33 +162,7 @@ private fun ExtensionDetails(
ScrollbarLazyColumn( ScrollbarLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
when { if (extension.isObsolete) {
extension.isFromExternalRepo ->
item {
val uriHandler = LocalUriHandler.current
val url = remember(extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
regex.find(extension.repoUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
?: extension.repoUrl
}
WarningBanner(
MR.strings.repo_extension_message,
modifier = Modifier.clickable {
url ?: return@clickable
uriHandler.openUri(url)
},
)
}
extension.isUnofficial ->
item {
WarningBanner(MR.strings.unofficial_extension_message)
}
extension.isObsolete ->
item { item {
WarningBanner(MR.strings.obsolete_extension_message) WarningBanner(MR.strings.obsolete_extension_message)
} }

View file

@ -342,7 +342,6 @@ private fun ExtensionItemContent(
val warning = when { val warning = when {
extension is Extension.Untrusted -> MR.strings.ext_untrusted extension is Extension.Untrusted -> MR.strings.ext_untrusted
extension is Extension.Installed && extension.isUnofficial -> MR.strings.ext_unofficial
extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
extension.isNsfw -> MR.strings.ext_nsfw_short extension.isNsfw -> MR.strings.ext_nsfw_short
else -> null else -> null

View file

@ -26,6 +26,7 @@ import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge import tachiyomi.presentation.core.components.Badge
@ -75,7 +76,7 @@ fun MigrateSourceScreen(
@Composable @Composable
private fun MigrateSourceList( private fun MigrateSourceList(
list: List<Pair<Source, Long>>, list: ImmutableList<Pair<Source, Long>>,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,

View file

@ -178,7 +178,7 @@ class ExtensionManager(
val pkgName = installedExt.pkgName val pkgName = installedExt.pkgName
val availableExt = availableExtensions.find { it.pkgName == pkgName } val availableExt = availableExtensions.find { it.pkgName == pkgName }
if (!installedExt.isUnofficial && availableExt == null && !installedExt.isObsolete) { if (availableExt == null && !installedExt.isObsolete) {
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true) mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
changed = true changed = true
} else if (availableExt != null) { } else if (availableExt != null) {
@ -187,13 +187,11 @@ class ExtensionManager(
if (installedExt.hasUpdate != hasUpdate) { if (installedExt.hasUpdate != hasUpdate) {
mutInstalledExtensions[index] = installedExt.copy( mutInstalledExtensions[index] = installedExt.copy(
hasUpdate = hasUpdate, hasUpdate = hasUpdate,
isFromExternalRepo = availableExt.isFromExternalRepo,
repoUrl = availableExt.repoUrl, repoUrl = availableExt.repoUrl,
) )
changed = true changed = true
} else if (availableExt.isFromExternalRepo) { } else {
mutInstalledExtensions[index] = installedExt.copy( mutInstalledExtensions[index] = installedExt.copy(
isFromExternalRepo = true,
repoUrl = availableExt.repoUrl, repoUrl = availableExt.repoUrl,
) )
changed = true changed = true
@ -363,7 +361,7 @@ class ExtensionManager(
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean { private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName } val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
if (isUnofficial || availableExt == null) return false ?: return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
} }

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.extension.api package eu.kanade.tachiyomi.extension.api
import android.content.Context import android.content.Context
import eu.kanade.domain.source.interactor.OFFICIAL_REPO_BASE_URL
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@ -36,14 +35,11 @@ internal class ExtensionApi {
suspend fun findExtensions(): List<Extension.Available> { suspend fun findExtensions(): List<Extension.Available> {
return withIOContext { return withIOContext {
val extensions = buildList { val extensions = sourcePreferences.extensionRepos().get().flatMap { getExtensions(it) }
addAll(getExtensions(OFFICIAL_REPO_BASE_URL, true))
sourcePreferences.extensionRepos().get().map { addAll(getExtensions(it, false)) }
}
// Sanity check - a small number of extensions probably means something broke // Sanity check - a small number of extensions probably means something broke
// with the repo generator // with the repo generator
if (extensions.size < 50) { if (extensions.isEmpty()) {
throw Exception() throw Exception()
} }
@ -51,10 +47,7 @@ internal class ExtensionApi {
} }
} }
private suspend fun getExtensions( private suspend fun getExtensions(repoBaseUrl: String): List<Extension.Available> {
repoBaseUrl: String,
isOfficialRepo: Boolean,
): List<Extension.Available> {
return try { return try {
val response = networkService.client val response = networkService.client
.newCall(GET("$repoBaseUrl/index.min.json")) .newCall(GET("$repoBaseUrl/index.min.json"))
@ -63,7 +56,7 @@ internal class ExtensionApi {
with(json) { with(json) {
response response
.parseAs<List<ExtensionJsonObject>>() .parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl, isRepoSource = !isOfficialRepo) .toExtensions(repoBaseUrl)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" } logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
@ -98,7 +91,7 @@ internal class ExtensionApi {
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion
val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer || hasUpdatedLib) val hasUpdate = hasUpdatedVer || hasUpdatedLib
if (hasUpdate) { if (hasUpdate) {
extensionsWithUpdate.add(installedExt) extensionsWithUpdate.add(installedExt)
} }
@ -111,10 +104,7 @@ internal class ExtensionApi {
return extensionsWithUpdate return extensionsWithUpdate
} }
private fun List<ExtensionJsonObject>.toExtensions( private fun List<ExtensionJsonObject>.toExtensions(repoUrl: String): List<Extension.Available> {
repoUrl: String,
isRepoSource: Boolean,
): List<Extension.Available> {
return this return this
.filter { .filter {
val libVersion = it.extractLibVersion() val libVersion = it.extractLibVersion()
@ -133,7 +123,6 @@ internal class ExtensionApi {
apkName = it.apk, apkName = it.apk,
iconUrl = "$repoUrl/icon/${it.pkg}.png", iconUrl = "$repoUrl/icon/${it.pkg}.png",
repoUrl = repoUrl, repoUrl = repoUrl,
isFromExternalRepo = isRepoSource,
) )
} }
} }

View file

@ -27,10 +27,8 @@ sealed class Extension {
val icon: Drawable?, val icon: Drawable?,
val hasUpdate: Boolean = false, val hasUpdate: Boolean = false,
val isObsolete: Boolean = false, val isObsolete: Boolean = false,
val isUnofficial: Boolean = false,
val isShared: Boolean, val isShared: Boolean,
val repoUrl: String? = null, val repoUrl: String? = null,
val isFromExternalRepo: Boolean = false,
) : Extension() ) : Extension()
data class Available( data class Available(
@ -45,7 +43,6 @@ sealed class Extension {
val apkName: String, val apkName: String,
val iconUrl: String, val iconUrl: String,
val repoUrl: String, val repoUrl: String,
val isFromExternalRepo: Boolean,
) : Extension() { ) : Extension() {
data class Source( data class Source(

View file

@ -59,9 +59,6 @@ internal object ExtensionLoader {
PackageManager.GET_SIGNATURES or PackageManager.GET_SIGNATURES or
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0) (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0)
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
private const val PRIVATE_EXTENSION_EXTENSION = "ext" private const val PRIVATE_EXTENSION_EXTENSION = "ext"
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts") private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
@ -255,7 +252,7 @@ internal object ExtensionLoader {
if (signatures.isNullOrEmpty()) { if (signatures.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error return LoadResult.Error
} else if (!isTrusted(pkgInfo, signatures)) { } else if (!trustExtension.isTrusted(pkgInfo, signatures.last())) {
val extension = Extension.Untrusted( val extension = Extension.Untrusted(
extName, extName,
pkgName, pkgName,
@ -323,7 +320,6 @@ internal object ExtensionLoader {
isNsfw = isNsfw, isNsfw = isNsfw,
sources = sources, sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY), pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = !isOfficiallySigned(signatures),
icon = appInfo.loadIcon(pkgManager), icon = appInfo.loadIcon(pkgManager),
isShared = extensionInfo.isShared, isShared = extensionInfo.isShared,
) )
@ -383,18 +379,6 @@ internal object ExtensionLoader {
?.toList() ?.toList()
} }
private fun isTrusted(pkgInfo: PackageInfo, signatures: List<String>): Boolean {
if (officialSignature in signatures) {
return true
}
return trustExtension.isTrusted(pkgInfo, signatures.last())
}
private fun isOfficiallySigned(signatures: List<String>): Boolean {
return signatures.all { it == officialSignature }
}
/** /**
* On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't * On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't
* have sourceDir which breaks assets loading (used for getting icon here). * have sourceDir which breaks assets loading (used for getting icon here).

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@ -30,13 +29,11 @@ data class ExtensionDetailsScreen(
} }
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val uriHandler = LocalUriHandler.current
ExtensionDetailsScreen( ExtensionDetailsScreen(
navigateUp = navigator::pop, navigateUp = navigator::pop,
state = state, state = state,
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) }, onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
onClickEnableAll = { screenModel.toggleSources(true) }, onClickEnableAll = { screenModel.toggleSources(true) },
onClickDisableAll = { screenModel.toggleSources(false) }, onClickDisableAll = { screenModel.toggleSources(false) },
onClickClearCookies = screenModel::clearCookies, onClickClearCookies = screenModel::clearCookies,

View file

@ -29,9 +29,6 @@ import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
private const val URL_EXTENSION_COMMITS =
"https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
class ExtensionDetailsScreenModel( class ExtensionDetailsScreenModel(
pkgName: String, pkgName: String,
context: Context, context: Context,
@ -86,16 +83,6 @@ class ExtensionDetailsScreenModel(
} }
} }
fun getChangelogUrl(): String {
val extension = state.value.extension ?: return ""
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
val pkgFactory = extension.pkgFactory
// Falling back on GitHub commit history because there is no explicit changelog in extension
return createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory)
}
fun clearCookies() { fun clearCookies() {
val extension = state.value.extension ?: return val extension = state.value.extension ?: return
@ -131,22 +118,6 @@ class ExtensionDetailsScreenModel(
?.let { toggleSource.await(it, enable) } ?.let { toggleSource.await(it, enable) }
} }
private fun createUrl(
url: String,
pkgName: String,
pkgFactory: String?,
path: String = "",
): String {
return if (!pkgFactory.isNullOrEmpty()) {
when (path.isEmpty()) {
true -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory"
else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path
}
} else {
url + "/src/" + pkgName.replace(".", "/") + path
}
}
@Immutable @Immutable
data class State( data class State(
val extension: Extension.Installed? = null, val extension: Extension.Installed? = null,

View file

@ -56,12 +56,12 @@ class CrashLogUtil(
val availableExtension = availableExtensions[it.pkgName] val availableExtension = availableExtensions[it.pkgName]
val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode val hasUpdate = (availableExtension?.versionCode ?: 0) > it.versionCode
if (!hasUpdate && !it.isObsolete && !it.isUnofficial) return@mapNotNull null if (!hasUpdate && !it.isObsolete) return@mapNotNull null
""" """
- ${it.name} - ${it.name}
Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"} Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"}
Obsolete: ${it.isObsolete} / Unofficial: ${it.isUnofficial} Obsolete: ${it.isObsolete}
""".trimIndent() """.trimIndent()
} }

View file

@ -43,8 +43,6 @@ fun Long.toDateKey(): Date {
return Date.from(instant.truncatedTo(ChronoUnit.DAYS)) return Date.from(instant.truncatedTo(ChronoUnit.DAYS))
} }
private const val MILLISECONDS_IN_DAY = 86_400_000L
fun Date.toRelativeString( fun Date.toRelativeString(
context: Context, context: Context,
relative: Boolean = true, relative: Boolean = true,
@ -69,6 +67,8 @@ fun Date.toRelativeString(
} }
} }
private const val MILLISECONDS_IN_DAY = 86_400_000L
private val Date.timeWithOffset: Long private val Date.timeWithOffset: Long
get() { get() {
return Calendar.getInstance().run { return Calendar.getInstance().run {
@ -78,6 +78,6 @@ private val Date.timeWithOffset: Long
} }
} }
fun Long.floorNearest(to: Long): Long { private fun Long.floorNearest(to: Long): Long {
return this.floorDiv(to) * to return this.floorDiv(to) * to
} }

View file

@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.source.model.SourceWithCount import tachiyomi.domain.source.model.SourceWithCount
@ -38,11 +39,12 @@ class SourceRepositoryImpl(
} }
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> { override fun getSourcesWithFavoriteCount(): Flow<List<Pair<DomainSource, Long>>> {
val sourceIdWithFavoriteCount = return combine(
handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() } handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() },
return sourceIdWithFavoriteCount.map { sourceIdsWithCount -> sourceManager.catalogueSources
sourceIdsWithCount ) { sourceIdWithFavoriteCount, _ -> sourceIdWithFavoriteCount }
.map { (sourceId, count) -> .map {
it.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId) val source = sourceManager.getOrStub(sourceId)
val domainSource = mapSourceToDomainSource(source).copy( val domainSource = mapSourceToDomainSource(source).copy(
isStub = source is StubSource, isStub = source is StubSource,

View file

@ -311,14 +311,12 @@
<string name="ext_installing">Installing</string> <string name="ext_installing">Installing</string>
<string name="ext_installed">Installed</string> <string name="ext_installed">Installed</string>
<string name="ext_trust">Trust</string> <string name="ext_trust">Trust</string>
<string name="ext_unofficial">Unofficial</string>
<string name="ext_untrusted">Untrusted</string> <string name="ext_untrusted">Untrusted</string>
<string name="ext_uninstall">Uninstall</string> <string name="ext_uninstall">Uninstall</string>
<string name="ext_app_info">App info</string> <string name="ext_app_info">App info</string>
<string name="untrusted_extension">Untrusted extension</string> <string name="untrusted_extension">Untrusted extension</string>
<string name="untrusted_extension_message">This extension was signed by any unknown author and wasn\'t loaded.\n\nMalicious extensions can read any stored login credentials or execute arbitrary code.\n\nBy trusting this extension\'s certificate, you accept these risks.</string> <string name="untrusted_extension_message">Malicious extensions can read any stored login credentials or execute arbitrary code.\n\nBy trusting this extension, you accept these risks.</string>
<string name="obsolete_extension_message">This extension is no longer available. It may not function properly and can cause issues with the app. Uninstalling it is recommended.</string> <string name="obsolete_extension_message">This extension is no longer available. It may not function properly and can cause issues with the app. Uninstalling it is recommended.</string>
<string name="unofficial_extension_message">This extension is not from the official repo.</string>
<string name="extension_api_error">Failed to fetch available extensions</string> <string name="extension_api_error">Failed to fetch available extensions</string>
<string name="ext_info_version">Version</string> <string name="ext_info_version">Version</string>
<string name="ext_info_language">Language</string> <string name="ext_info_language">Language</string>
@ -346,7 +344,7 @@
<string name="action_delete_repo">Delete repo</string> <string name="action_delete_repo">Delete repo</string>
<string name="invalid_repo_name">Invalid repo URL</string> <string name="invalid_repo_name">Invalid repo URL</string>
<string name="delete_repo_confirmation">Do you wish to delete the repo \"%s\"?</string> <string name="delete_repo_confirmation">Do you wish to delete the repo \"%s\"?</string>
<string name="repo_extension_message">This extension is from an external repo. Tap to view the repo.</string> <string name="action_open_repo">Open source repo</string>
<!-- Reader section --> <!-- Reader section -->
<string name="pref_fullscreen">Fullscreen</string> <string name="pref_fullscreen">Fullscreen</string>