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 {
// Do not allow invalid formats
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) {
if (!name.matches(repoRegex)) {
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()

View file

@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
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.material3.AlertDialog
import androidx.compose.material3.Button
@ -67,13 +67,23 @@ fun ExtensionDetailsScreen(
navigateUp: () -> Unit,
state: ExtensionDetailsScreenModel.State,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickWhatsNew: () -> Unit,
onClickEnableAll: () -> Unit,
onClickDisableAll: () -> Unit,
onClickClearCookies: () -> Unit,
onClickUninstall: () -> 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(
topBar = { scrollBehavior ->
AppBar(
@ -83,12 +93,14 @@ fun ExtensionDetailsScreen(
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (state.extension?.isUnofficial == false) {
if (url != null) {
add(
AppBar.Action(
title = stringResource(MR.strings.whats_new),
icon = Icons.Outlined.History,
onClick = onClickWhatsNew,
title = stringResource(MR.strings.action_open_repo),
icon = Icons.AutoMirrored.Outlined.Launch,
onClick = {
uriHandler.openUri(url)
},
),
)
}
@ -150,36 +162,10 @@ private fun ExtensionDetails(
ScrollbarLazyColumn(
contentPadding = contentPadding,
) {
when {
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 {
WarningBanner(MR.strings.obsolete_extension_message)
}
if (extension.isObsolete) {
item {
WarningBanner(MR.strings.obsolete_extension_message)
}
}
item {

View file

@ -342,7 +342,6 @@ private fun ExtensionItemContent(
val warning = when {
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.isNsfw -> MR.strings.ext_nsfw_short
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.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge
@ -75,7 +76,7 @@ fun MigrateSourceScreen(
@Composable
private fun MigrateSourceList(
list: List<Pair<Source, Long>>,
list: ImmutableList<Pair<Source, Long>>,
contentPadding: PaddingValues,
onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,

View file

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

View file

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

View file

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

View file

@ -59,9 +59,6 @@ internal object ExtensionLoader {
PackageManager.GET_SIGNATURES or
(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 fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
@ -255,7 +252,7 @@ internal object ExtensionLoader {
if (signatures.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error
} else if (!isTrusted(pkgInfo, signatures)) {
} else if (!trustExtension.isTrusted(pkgInfo, signatures.last())) {
val extension = Extension.Untrusted(
extName,
pkgName,
@ -323,7 +320,6 @@ internal object ExtensionLoader {
isNsfw = isNsfw,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = !isOfficiallySigned(signatures),
icon = appInfo.loadIcon(pkgManager),
isShared = extensionInfo.isShared,
)
@ -383,18 +379,6 @@ internal object ExtensionLoader {
?.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
* 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.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
@ -30,13 +29,11 @@ data class ExtensionDetailsScreen(
}
val navigator = LocalNavigator.currentOrThrow
val uriHandler = LocalUriHandler.current
ExtensionDetailsScreen(
navigateUp = navigator::pop,
state = state,
onClickSourcePreferences = { navigator.push(SourcePreferencesScreen(it)) },
onClickWhatsNew = { uriHandler.openUri(screenModel.getChangelogUrl()) },
onClickEnableAll = { screenModel.toggleSources(true) },
onClickDisableAll = { screenModel.toggleSources(false) },
onClickClearCookies = screenModel::clearCookies,

View file

@ -29,9 +29,6 @@ import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
private const val URL_EXTENSION_COMMITS =
"https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
class ExtensionDetailsScreenModel(
pkgName: String,
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() {
val extension = state.value.extension ?: return
@ -131,22 +118,6 @@ class ExtensionDetailsScreenModel(
?.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
data class State(
val extension: Extension.Installed? = null,

View file

@ -56,12 +56,12 @@ class CrashLogUtil(
val availableExtension = availableExtensions[it.pkgName]
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}
Installed: ${it.versionName} / Available: ${availableExtension?.versionName ?: "?"}
Obsolete: ${it.isObsolete} / Unofficial: ${it.isUnofficial}
Obsolete: ${it.isObsolete}
""".trimIndent()
}

View file

@ -43,8 +43,6 @@ fun Long.toDateKey(): Date {
return Date.from(instant.truncatedTo(ChronoUnit.DAYS))
}
private const val MILLISECONDS_IN_DAY = 86_400_000L
fun Date.toRelativeString(
context: Context,
relative: Boolean = true,
@ -69,6 +67,8 @@ fun Date.toRelativeString(
}
}
private const val MILLISECONDS_IN_DAY = 86_400_000L
private val Date.timeWithOffset: Long
get() {
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
}

View file

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

View file

@ -311,14 +311,12 @@
<string name="ext_installing">Installing</string>
<string name="ext_installed">Installed</string>
<string name="ext_trust">Trust</string>
<string name="ext_unofficial">Unofficial</string>
<string name="ext_untrusted">Untrusted</string>
<string name="ext_uninstall">Uninstall</string>
<string name="ext_app_info">App info</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="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="ext_info_version">Version</string>
<string name="ext_info_language">Language</string>
@ -346,7 +344,7 @@
<string name="action_delete_repo">Delete repo</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="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 -->
<string name="pref_fullscreen">Fullscreen</string>