diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt b/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt index f55532e12..fc99f4e66 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 213fe9dd2..e55576db8 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -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().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 { diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index 5b641235d..1b8b48a1e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index e45ca5189..404d11476 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -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>, + list: ImmutableList>, contentPadding: PaddingValues, onClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit, diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 156d881c8..6b7594e86 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt index d0d493a97..185f7cefd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt @@ -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 { 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 { + private suspend fun getExtensions(repoBaseUrl: String): List { return try { val response = networkService.client .newCall(GET("$repoBaseUrl/index.min.json")) @@ -63,7 +56,7 @@ internal class ExtensionApi { with(json) { response .parseAs>() - .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.toExtensions( - repoUrl: String, - isRepoSource: Boolean, - ): List { + private fun List.toExtensions(repoUrl: String): List { 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, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index 4982ef6e3..a8e80d0a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 728773ebe..6c765edf5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -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): Boolean { - if (officialSignature in signatures) { - return true - } - - return trustExtension.isTrusted(pkgInfo, signatures.last()) - } - - private fun isOfficiallySigned(signatures: List): 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). diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt index 79af92328..4a4a78cde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreen.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt index c6e821bbd..8d8a9f607 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsScreenModel.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt index 7f39823e7..4b2ef2ab1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -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() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt index ab109c49b..e7482165f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt @@ -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 } diff --git a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt index e8cf8f43c..65bf33ac0 100644 --- a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt @@ -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>> { - 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> { diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 5da0b3a54..ccb9e8721 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -311,14 +311,12 @@ Installing Installed Trust - Unofficial Untrusted Uninstall App info Untrusted extension - 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. + Malicious extensions can read any stored login credentials or execute arbitrary code.\n\nBy trusting this extension, you accept these risks. This extension is no longer available. It may not function properly and can cause issues with the app. Uninstalling it is recommended. - This extension is not from the official repo. Failed to fetch available extensions Version Language @@ -346,7 +344,7 @@ Delete repo Invalid repo URL Do you wish to delete the repo \"%s\"? - This extension is from an external repo. Tap to view the repo. + Open source repo Fullscreen