diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8c252504b..9a7611bc7 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ I acknowledge that: - I have updated: - - To the latest version of the app (stable is v0.15.0) + - To the latest version of the app (stable is v0.15.1) - All extensions - I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/) - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index 9bacda224..81f73fa09 100644 --- a/.github/ISSUE_TEMPLATE/report_issue.yml +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -53,7 +53,7 @@ body: label: Tachiyomi version description: You can find your Tachiyomi version in **More → About**. placeholder: | - Example: "0.15.0" + Example: "0.15.1" validations: required: true @@ -98,7 +98,7 @@ body: required: true - label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/). required: true - - label: I have updated the app to version **[0.15.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. + - label: I have updated the app to version **[0.15.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml index 86fc24aea..d3d2f7190 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -33,7 +33,7 @@ body: required: true - label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose). required: true - - label: I have updated the app to version **[0.15.0](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. + - label: I have updated the app to version **[0.15.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bf621c148..31643a698 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,8 +22,8 @@ android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 115 - versionName = "0.15.0" + versionCode = 117 + versionName = "0.15.1" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 78b43e888..1fc53d0b7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,8 @@ - @@ -21,17 +22,20 @@ - - - + - + android:theme="@style/Theme.Tachiyomi.SplashScreen"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:exported="false" + android:process=":error_handler" /> + android:launchMode="singleTask" + android:theme="@android:style/Theme.NoDisplay"> @@ -99,20 +143,21 @@ + android:exported="false" + android:launchMode="singleTask"> - + + android:exported="false" + android:theme="@style/Theme.Tachiyomi" /> + android:exported="false" + android:theme="@android:style/Theme.Translucent.NoTitleBar" /> + android:exported="true" + android:label="@string/track_activity_name"> - - - - + - + + + + + // Remove previously trusted versions + val removed = exts.filter { it.startsWith("$pkgName:") }.toMutableSet() + + removed.also { + it += "$pkgName:$versionCode:$signatureHash" + } + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index ea00bfc69..5779020a2 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -38,11 +38,14 @@ class SourcePreferences( SetMigrateSorting.Direction.ASCENDING, ) + fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) + fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet()) fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) - fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet()) - - fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) + fun trustedExtensions() = preferenceStore.getStringSet( + Preference.appStateKey("trusted_extensions"), + emptySet(), + ) } 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 44ba8167c..5b641235d 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -133,13 +133,13 @@ private fun ExtensionContent( ) { val context = LocalContext.current var trustState by remember { mutableStateOf(null) } - val installGranted = rememberRequestPackageInstallsPermissionState() + val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true) FastScrollLazyColumn( contentPadding = contentPadding + topSmallPaddingValues, ) { if (!installGranted && state.installer?.requiresSystemPermission == true) { - item { + item(key = "extension-permissions-warning") { WarningBanner( textRes = MR.strings.ext_permission_install_apps_warning, modifier = Modifier.clickable { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt index 0cf4f027d..b9247f807 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt @@ -16,16 +16,22 @@ import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.flow.collectLatest import tachiyomi.presentation.core.screens.LoadingScreen -class ExtensionReposScreen : Screen() { +class ExtensionReposScreen( + private val url: String? = null, +) : Screen() { @Composable override fun Content() { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { ExtensionReposScreenModel() } + val screenModel = rememberScreenModel { ExtensionReposScreenModel() } val state by screenModel.state.collectAsState() + LaunchedEffect(url) { + url?.let { screenModel.createRepo(it) } + } + if (state is RepoScreenState.Loading) { LoadingScreen() return diff --git a/app/src/main/java/eu/kanade/presentation/util/Permissions.kt b/app/src/main/java/eu/kanade/presentation/util/Permissions.kt index e3f2df2c8..b4f4f4bd3 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Permissions.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Permissions.kt @@ -14,11 +14,11 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @Composable -fun rememberRequestPackageInstallsPermissionState(): Boolean { +fun rememberRequestPackageInstallsPermissionState(initialValue: Boolean = false): Boolean { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - var installGranted by remember { mutableStateOf(false) } + var installGranted by remember { mutableStateOf(initialValue) } DisposableEffect(lifecycleOwner.lifecycle) { val observer = object : DefaultLifecycleObserver { diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 46f7e3812..9947f98ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -374,13 +374,6 @@ object Migrations { uiPreferences.relativeTime().set(false) } } - if (oldVersion < 107) { - replacePreferences( - preferenceStore = preferenceStore, - filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }, - newKey = { Preference.privateKey(it) }, - ) - } if (oldVersion < 113) { val prefsToReplace = listOf( "pref_download_only", @@ -407,7 +400,19 @@ object Migrations { } if (oldVersion < 114) { sourcePreferences.extensionRepos().getAndSet { - it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet() + it.map { repo -> "https://raw.githubusercontent.com/$repo/repo" }.toSet() + } + } + if (oldVersion < 116) { + replacePreferences( + preferenceStore = preferenceStore, + filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }, + newKey = { Preference.privateKey(it) }, + ) + } + if (oldVersion < 117) { + prefs.edit { + remove(Preference.appStateKey("trusted_signatures")) } } return true diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt index a5c9be8be..a3f6852fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt @@ -31,6 +31,7 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException import java.util.Date +import kotlin.system.measureTimeMillis /** * A manager to handle synchronization tasks in the app, such as updating @@ -122,8 +123,10 @@ class SyncManager( val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup) updateNonFavorites(nonFavorites) + val mangas = processFavoriteManga(filteredFavorites) + val newSyncData = backup.copy( - backupManga = filteredFavorites, + backupManga = mangas, backupCategories = remoteBackup.backupCategories, backupSources = remoteBackup.backupSources, backupPreferences = remoteBackup.backupPreferences, @@ -131,7 +134,7 @@ class SyncManager( ) // It's local sync no need to restore data. (just update remote data) - if (filteredFavorites.isEmpty()) { + if (mangas.isEmpty()) { // update the sync timestamp syncPreferences.lastSyncTimestamp().set(Date().time) return @@ -150,6 +153,9 @@ class SyncManager( library = true, ), ) + + // update the sync timestamp + syncPreferences.lastSyncTimestamp().set(Date().time) } else { logcat(LogPriority.ERROR) { "Failed to write sync data to file" } } @@ -185,10 +191,6 @@ class SyncManager( return localManga.source != remoteManga.source || localManga.url != remoteManga.url || localManga.title != remoteManga.title || - localManga.artist != remoteManga.artist || - localManga.author != remoteManga.author || - localManga.description != remoteManga.description || - localManga.genre != remoteManga.genre || localManga.status.toInt() != remoteManga.status || localManga.thumbnailUrl != remoteManga.thumbnailUrl || localManga.dateAdded != remoteManga.dateAdded || @@ -217,15 +219,10 @@ class SyncManager( val localChapter = localChapterMap[remoteChapter.url] localChapter == null || // No corresponding local chapter localChapter.url != remoteChapter.url || - localChapter.name != remoteChapter.name || - localChapter.scanlator != remoteChapter.scanlator || localChapter.read != remoteChapter.read || localChapter.bookmark != remoteChapter.bookmark || localChapter.last_page_read != remoteChapter.lastPageRead || - localChapter.chapter_number != remoteChapter.chapterNumber || - localChapter.source_order != remoteChapter.sourceOrder || - localChapter.date_fetch != remoteChapter.dateFetch || - localChapter.date_upload != remoteChapter.dateUpload + localChapter.chapter_number != remoteChapter.chapterNumber } } @@ -235,26 +232,90 @@ class SyncManager( * @return a Pair of lists, where the first list contains different favorite manga and the second list contains non-favorite manga. */ private suspend fun filterFavoritesAndNonFavorites(backup: Backup): Pair, List> { - val databaseMangaFavorites = getFavorites.await() - val localMangaMap = databaseMangaFavorites.associateBy { it.url } val favorites = mutableListOf() val nonFavorites = mutableListOf() + val elapsedTimeMillis = measureTimeMillis { + val databaseMangaFavorites = getFavorites.await() + val localMangaMap = databaseMangaFavorites.associateBy { it.url } - backup.backupManga.forEach { remoteManga -> - if (remoteManga.favorite) { - localMangaMap[remoteManga.url]?.let { localManga -> - if (isMangaDifferent(localManga, remoteManga)) { - favorites.add(remoteManga) + logcat(LogPriority.DEBUG) { "Starting to filter favorites and non-favorites from backup data." } + + backup.backupManga.forEach { remoteManga -> + val localManga = localMangaMap[remoteManga.url] + when { + // Checks if the manga is in favorites and needs updating or adding + remoteManga.favorite -> { + if (localManga == null || isMangaDifferent(localManga, remoteManga)) { + logcat(LogPriority.DEBUG) { "Adding to favorites: ${remoteManga.title}" } + favorites.add(remoteManga) + } else { + logcat(LogPriority.DEBUG) { "Already up-to-date favorite: ${remoteManga.title}" } + } } - } ?: favorites.add(remoteManga) - } else { - nonFavorites.add(remoteManga) + // Handle non-favorites + !remoteManga.favorite -> { + logcat(LogPriority.DEBUG) { "Adding to non-favorites: ${remoteManga.title}" } + nonFavorites.add(remoteManga) + } + } } } + val minutes = elapsedTimeMillis / 60000 + val seconds = (elapsedTimeMillis % 60000) / 1000 + logcat(LogPriority.DEBUG) { + "Filtering completed in ${minutes}m ${seconds}s. Favorites found: ${favorites.size}, " + + "Non-favorites found: ${nonFavorites.size}" + } + return Pair(favorites, nonFavorites) } + private fun processFavoriteManga(backupManga: List): List { + val mangas = mutableListOf() + val lastSyncTimeStamp = syncPreferences.lastSyncTimestamp().get() + + val elapsedTimeMillis = measureTimeMillis { + logcat(LogPriority.DEBUG) { "Starting to process BackupMangas." } + backupManga.forEach { manga -> + val mangaLastUpdatedStatus = manga.lastModifiedAt * 1000L > lastSyncTimeStamp + val chaptersUpdatedStatus = chaptersUpdatedAfterSync(manga, lastSyncTimeStamp) + + if (mangaLastUpdatedStatus || chaptersUpdatedStatus) { + mangas.add(manga) + logcat(LogPriority.DEBUG) { + "Added ${manga.title} to the process list. Manga Last Updated: $mangaLastUpdatedStatus, " + + "Chapters Updated: $chaptersUpdatedStatus." + } + } else { + logcat(LogPriority.DEBUG) { + "Skipped ${manga.title} as it has not been updated since the last sync " + + "(Last Modified: ${manga.lastModifiedAt * 1000L}, Last Sync: $lastSyncTimeStamp)." + } + } + } + } + + val minutes = elapsedTimeMillis / 60000 + val seconds = (elapsedTimeMillis % 60000) / 1000 + logcat(LogPriority.DEBUG) { "Processing completed in ${minutes}m ${seconds}s. Total Processed: ${mangas.size}" } + + return mangas + } + + private fun chaptersUpdatedAfterSync(manga: BackupManga, lastSyncTimeStamp: Long): Boolean { + return manga.chapters.any { chapter -> + val updated = chapter.lastModifiedAt * 1000L > lastSyncTimeStamp + if (updated) { + logcat(LogPriority.DEBUG) { + "Chapter ${chapter.name} of ${manga.title} updated after last sync " + + "(Chapter Last Modified: ${chapter.lastModifiedAt}, Last Sync: $lastSyncTimeStamp)." + } + } + updated + } + } + /** * Updates the non-favorite manga in the local database with their favorite status from the backup. * @param nonFavorites the list of non-favorite BackupManga objects from the backup. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt index 38701ebe1..1fd55495d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/service/SyncService.kt @@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupManga import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import logcat.LogPriority +import logcat.logcat import tachiyomi.domain.sync.SyncPreferences import java.time.Instant @@ -92,37 +94,99 @@ abstract class SyncService( localMangaList: List?, remoteMangaList: List?, ): List { + val logTag = "MergeMangaLists" + // Convert null lists to empty to simplify logic val localMangaListSafe = localMangaList.orEmpty() val remoteMangaListSafe = remoteMangaList.orEmpty() - // Associate both local and remote manga by their unique keys (source and url) - val localMangaMap = localMangaListSafe.associateBy { Pair(it.source, it.url) } - val remoteMangaMap = remoteMangaListSafe.associateBy { Pair(it.source, it.url) } + logcat(logTag, LogPriority.DEBUG) { + "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" + } + + // Define a function to create a composite key from manga + fun mangaCompositeKey(manga: BackupManga): String { + return "${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}" + } + + // Create maps using composite keys + val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) } + val remoteMangaMap = remoteMangaListSafe.associateBy { mangaCompositeKey(it) } + + logcat(LogPriority.DEBUG, logTag) { + "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" + } // Prepare to merge both sets of manga - return (localMangaMap.keys + remoteMangaMap.keys).mapNotNull { key -> - val local = localMangaMap[key] - val remote = remoteMangaMap[key] + val mergedList = (localMangaMap.keys + remoteMangaMap.keys).distinct().mapNotNull { compositeKey -> + val local = localMangaMap[compositeKey] + val remote = remoteMangaMap[compositeKey] + + logcat(LogPriority.DEBUG, logTag) { + "Processing key: $compositeKey. Local favorite: ${local?.favorite}, " + + "Remote favorite: ${remote?.favorite}" + } when { - local != null && remote == null -> local - local == null && remote != null -> remote + local != null && remote == null -> { + logcat(LogPriority.DEBUG, logTag) { + "Taking local manga: ${local.title} as it is not present remotely. " + + "Favorite status: ${local.favorite}" + } + local + } + local == null && remote != null -> { + logcat(LogPriority.DEBUG, logTag) { + "Taking remote manga: ${remote.title} as it is not present locally. " + + "Favorite status: ${remote.favorite}" + } + remote + } local != null && remote != null -> { - // Compare last modified times and merge chapters - val localTime = Instant.ofEpochMilli(local.lastModifiedAt) - val remoteTime = Instant.ofEpochMilli(remote.lastModifiedAt) + logcat(LogPriority.DEBUG, logTag) { + "Inspecting timestamps for ${local.title}. Local lastModifiedAt: ${local.lastModifiedAt}, " + + "Remote lastModifiedAt: ${remote.lastModifiedAt}" + } + // Convert seconds to milliseconds for accurate time comparison + val localTime = Instant.ofEpochMilli(local.lastModifiedAt * 1000L) + val remoteTime = Instant.ofEpochMilli(remote.lastModifiedAt * 1000L) val mergedChapters = mergeChapters(local.chapters, remote.chapters) + logcat(LogPriority.DEBUG, logTag) { + "Merging manga: ${local.title}. Local time: $localTime, Remote time: $remoteTime, " + + "Local favorite: ${local.favorite}, Remote favorite: ${remote.favorite}" + } + if (localTime >= remoteTime) { + logcat( + LogPriority.DEBUG, + logTag, + ) { "Keeping local version of ${local.title} with merged chapters." } local.copy(chapters = mergedChapters) } else { + logcat( + LogPriority.DEBUG, + logTag, + ) { "Keeping remote version of ${remote.title} with merged chapters." } remote.copy(chapters = mergedChapters) } } - else -> null + else -> { + logcat(LogPriority.DEBUG, logTag) { "No manga found for key: $compositeKey. Skipping." } + null + } } } + + // Counting favorites and non-favorites + val (favorites, nonFavorites) = mergedList.partition { it.favorite } + + logcat(LogPriority.DEBUG, logTag) { + "Merge completed. Total merged manga: ${mergedList.size}, Favorites: ${favorites.size}, " + + "Non-Favorites: ${nonFavorites.size}" + } + + return mergedList } /** @@ -146,27 +210,64 @@ abstract class SyncService( localChapters: List, remoteChapters: List, ): List { - // Associate chapters by URL for both local and remote - val localChapterMap = localChapters.associateBy { it.url } - val remoteChapterMap = remoteChapters.associateBy { it.url } + val logTag = "MergeChapters" + + // Define a function to create a composite key from a chapter + fun chapterCompositeKey(chapter: BackupChapter): String { + return "${chapter.url}|${chapter.name}|${chapter.chapterNumber}" + } + + // Create maps using composite keys + val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) } + val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) } + + logcat(LogPriority.DEBUG, logTag) { + "Starting chapter merge. Local chapters: ${localChapters.size}, Remote chapters: ${remoteChapters.size}" + } // Merge both chapter maps - return (localChapterMap.keys + remoteChapterMap.keys).mapNotNull { url -> - // Determine the most recent chapter by comparing lastModifiedAt, considering null as Instant.MIN - val localChapter = localChapterMap[url] - val remoteChapter = remoteChapterMap[url] + val mergedChapters = (localChapterMap.keys + remoteChapterMap.keys).distinct().mapNotNull { compositeKey -> + val localChapter = localChapterMap[compositeKey] + val remoteChapter = remoteChapterMap[compositeKey] + + logcat(LogPriority.DEBUG, logTag) { + "Processing chapter key: $compositeKey. Local chapter: ${localChapter != null}, " + + "Remote chapter: ${remoteChapter != null}" + } when { - localChapter != null && remoteChapter == null -> localChapter - localChapter == null && remoteChapter != null -> remoteChapter - localChapter != null && remoteChapter != null -> { - val localInstant = localChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) } ?: Instant.MIN - val remoteInstant = remoteChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) } ?: Instant.MIN - if (localInstant >= remoteInstant) localChapter else remoteChapter + localChapter != null && remoteChapter == null -> { + logcat(LogPriority.DEBUG, logTag) { "Keeping local chapter: ${localChapter.name}." } + localChapter + } + localChapter == null && remoteChapter != null -> { + logcat(LogPriority.DEBUG, logTag) { "Taking remote chapter: ${remoteChapter.name}." } + remoteChapter + } + localChapter != null && remoteChapter != null -> { + val localInstant = Instant.ofEpochMilli(localChapter.lastModifiedAt * 1000L) + val remoteInstant = Instant.ofEpochMilli(remoteChapter.lastModifiedAt * 1000L) + + val chosenChapter = if (localInstant >= remoteInstant) localChapter else remoteChapter + logcat(LogPriority.DEBUG, logTag) { + "Merging chapter: ${chosenChapter.name}. Chosen from: ${if (localInstant >= remoteInstant) { + "Local" + } else { + "Remote" + }}." + } + chosenChapter + } + else -> { + logcat(LogPriority.DEBUG, logTag) { "No chapter found for composite key: $compositeKey. Skipping." } + null } - else -> null } } + + logcat(LogPriority.DEBUG, logTag) { "Chapter merge completed. Total merged chapters: ${mergedChapters.size}" } + + return mergedChapters } /** 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 4c342dd3d..942e8f4a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension import android.content.Context import android.graphics.drawable.Drawable +import eu.kanade.domain.source.interactor.TrustExtension import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.api.ExtensionApi import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier @@ -18,7 +19,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import logcat.LogPriority -import tachiyomi.core.preference.plusAssign import tachiyomi.core.util.lang.launchNow import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat @@ -34,13 +34,11 @@ import java.util.Locale * To avoid malicious distribution, every extension must be signed and it will only be loaded if its * signature is trusted, otherwise the user will be prompted with a warning to trust it before being * loaded. - * - * @param context The application context. - * @param preferences The application preferences. */ class ExtensionManager( private val context: Context, private val preferences: SourcePreferences = Injekt.get(), + private val trustExtension: TrustExtension = Injekt.get(), ) { var isInitialized = false @@ -249,18 +247,19 @@ class ExtensionManager( } /** - * Adds the given signature to the list of trusted signatures. It also loads in background the - * extensions that match this signature. + * Adds the given extension to the list of trusted extensions. It also loads in background the + * now trusted extensions. * - * @param signature The signature to whitelist. + * @param extension the extension to trust */ - fun trustSignature(signature: String) { - val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet() - if (signature !in untrustedSignatures) return + fun trust(extension: Extension.Untrusted) { + val untrustedPkgNames = _untrustedExtensionsFlow.value.map { it.pkgName }.toSet() + if (extension.pkgName !in untrustedPkgNames) return - preferences.trustedSignatures() += signature + trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash) - val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature } + val nowTrustedExtensions = _untrustedExtensionsFlow.value + .filter { it.pkgName == extension.pkgName && it.versionCode == extension.versionCode } _untrustedExtensionsFlow.value -= nowTrustedExtensions launchNow { 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 a01ee5cb4..728773ebe 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 @@ -7,6 +7,7 @@ import android.content.pm.PackageManager import android.os.Build import androidx.core.content.pm.PackageInfoCompat import dalvik.system.PathClassLoader +import eu.kanade.domain.source.interactor.TrustExtension import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult @@ -15,7 +16,6 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.util.lang.Hash import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo -import eu.kanade.tachiyomi.util.system.isDevFlavor import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking @@ -41,6 +41,7 @@ import java.io.File internal object ExtensionLoader { private val preferences: SourcePreferences by injectLazy() + private val trustExtension: TrustExtension by injectLazy() private val loadNsfwSource by lazy { preferences.showNsfwSource().get() } @@ -49,8 +50,6 @@ internal object ExtensionLoader { private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" private const val METADATA_NSFW = "tachiyomi.extension.nsfw" - private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme" - private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog" const val LIB_VERSION_MIN = 1.4 const val LIB_VERSION_MAX = 1.5 @@ -119,12 +118,6 @@ internal object ExtensionLoader { * @param context The application context. */ fun loadExtensions(context: Context): List { - // Always make users trust unknown extensions on cold starts in non-dev builds - // due to inherent security risks - if (!isDevFlavor) { - preferences.trustedSignatures().delete() - } - val pkgManager = context.packageManager val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -262,7 +255,7 @@ internal object ExtensionLoader { if (signatures.isNullOrEmpty()) { logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } return LoadResult.Error - } else if (!hasTrustedSignature(signatures)) { + } else if (!isTrusted(pkgInfo, signatures)) { val extension = Extension.Untrusted( extName, pkgName, @@ -281,9 +274,6 @@ internal object ExtensionLoader { return LoadResult.Error } - val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1 - val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1 - val classLoader = try { PathClassLoader(appInfo.sourceDir, null, context.classLoader) } catch (e: Exception) { @@ -393,13 +383,12 @@ internal object ExtensionLoader { ?.toList() } - private fun hasTrustedSignature(signatures: List): Boolean { + private fun isTrusted(pkgInfo: PackageInfo, signatures: List): Boolean { if (officialSignature in signatures) { return true } - val trustedSignatures = preferences.trustedSignatures().get() - return trustedSignatures.any { signatures.contains(it) } + return trustExtension.isTrusted(pkgInfo, signatures.last()) } private fun isOfficiallySigned(signatures: List): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index f6ed811a7..5617e506b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -195,8 +195,8 @@ class ExtensionsScreenModel( } } - fun trustSignature(signatureHash: String) { - extensionManager.trustSignature(signatureHash) + fun trustExtension(extension: Extension.Untrusted) { + extensionManager.trust(extension) } @Immutable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt index a5b390a89..635c6cb83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt @@ -61,7 +61,7 @@ fun extensionsTab( }, onInstallExtension = extensionsScreenModel::installExtension, onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) }, - onTrustExtension = { extensionsScreenModel.trustSignature(it.signatureHash) }, + onTrustExtension = { extensionsScreenModel.trustExtension(it) }, onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) }, onUpdateExtension = extensionsScreenModel::updateExtension, onRefresh = extensionsScreenModel::findAvailableExtensions, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 3ad313c4f..3b5fe77d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -56,6 +56,8 @@ import eu.kanade.presentation.components.AppStateBanners import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor import eu.kanade.presentation.components.IndexingBannerBackgroundColor +import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.DefaultNavigatorScreenTransition import eu.kanade.tachiyomi.BuildConfig @@ -444,6 +446,21 @@ class MainActivity : BaseActivity() { } null } + Intent.ACTION_VIEW -> { + // Handling opening of backup files + if (intent.data.toString().endsWith(".tachibk")) { + navigator.popUntilRoot() + navigator.push(RestoreBackupScreen(intent.data.toString())) + } + // Deep link to add extension repo + else if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") { + intent.data?.getQueryParameter("url")?.let { repoUrl -> + navigator.popUntilRoot() + navigator.push(ExtensionReposScreen(repoUrl)) + } + } + null + } else -> return false } diff --git a/core/src/main/java/tachiyomi/core/util/system/LogcatExtensions.kt b/core/src/main/java/tachiyomi/core/util/system/LogcatExtensions.kt index fb587b07d..eaf7971d4 100644 --- a/core/src/main/java/tachiyomi/core/util/system/LogcatExtensions.kt +++ b/core/src/main/java/tachiyomi/core/util/system/LogcatExtensions.kt @@ -7,12 +7,22 @@ import logcat.logcat inline fun Any.logcat( priority: LogPriority = LogPriority.DEBUG, throwable: Throwable? = null, + tag: String? = null, message: () -> String = { "" }, ) = logcat(priority = priority) { - var msg = message() - if (throwable != null) { - if (msg.isNotBlank()) msg += "\n" - msg += throwable.asLog() + val logMessage = StringBuilder() + + if (!tag.isNullOrEmpty()) { + logMessage.append("[$tag] ") } - msg + + val msg = message() + logMessage.append(msg) + + if (throwable != null) { + if (msg.isNotBlank()) logMessage.append("\n") + logMessage.append(throwable.asLog()) + } + + logMessage.toString() } diff --git a/i18n/src/commonMain/resources/MR/es/strings.xml b/i18n/src/commonMain/resources/MR/es/strings.xml index ecad0477e..de0f08303 100644 --- a/i18n/src/commonMain/resources/MR/es/strings.xml +++ b/i18n/src/commonMain/resources/MR/es/strings.xml @@ -9,7 +9,7 @@ Copia de seguridad y restauración Ajustes Filtrar - Sin leer + Con capítulos sin leer Quitar filtro Buscar Seleccionar todo @@ -54,7 +54,7 @@ Cada 2 días Restricciones de actualización automática del dispositivo Mientras se carga la batería - Omitir entradas marcadas como \"Completado\" + Saltarse títulos cuya publicación haya concluido Actualizar progreso al terminar un capítulo Pantalla completa Transiciones de página animadas @@ -98,8 +98,8 @@ Cookies borradas Borrar la base de datos Borrar el historial de los elementos que no estén guardados en tu biblioteca - ¿Está seguro\? Se perderán los capítulos leídos y el progreso de las entradas que no pertenezcan a la biblioteca - Entradas eliminadas + ¿Seguro? Perderás los capítulos leídos y el progreso de los títulos que no pertenezcan a la biblioteca + Elementos borrados Versión Enviar informes de fallos Ayuda a corregir cualquier error. No se enviará ninguna información personal @@ -126,7 +126,7 @@ Sin leer ¿Seguro que quieres eliminar los capítulos seleccionados\? Leyendo - Completado + Concluido Abandonado En espera Para leer luego @@ -141,7 +141,7 @@ No se pudo cargar la imagen No se pudieron descargar los capítulos. Puedes volver a intentarlo en la sección de descargas Nuevos capítulos encontrados - Primero tienes que añadir el manga a tu biblioteca + Primero tienes que añadir el título a tu biblioteca Elige una imagen de portada Elige una copia de respaldo Descargar @@ -158,7 +158,7 @@ No se ha podido descargar el capítulo debido a un error inesperado No estás conectado a ninguna red Wi-Fi Categorías - Entradas en la biblioteca + Títulos en la biblioteca Seguimiento Historial Favoritos @@ -211,7 +211,7 @@ Creando copia de seguridad No hay más resultados Fuente local - ¿Eliminar los capítulos descargados\? + ¿Quieres borrar los capítulos descargados? Pausado Seguimiento ¡Ya existe una categoría con este nombre! @@ -240,7 +240,7 @@ Descargando Instalando Instalado - Confiable + Confiar No confiable Desinstalar Extensión no confiable @@ -252,11 +252,11 @@ No hay ningún capítulo anterior Migrar Copiar - Esta extensión fue firmada por un autor desconocido y no fue cargada. + Esta extensión tiene una firma de un autor desconocido y no se ha podido cargar. \n -\nLas extensiones maliciosas pueden leer cualquier credencial de inicio de sesión almacenada o ejecutar código arbitrario. +\nUna extensión maliciosa puede leer credenciales de inicio guardadas o ejecutar cualquier tipo de código en tu dispositivo. \n -\nAl confiar en el certificado de esta extensión, aceptas estos riesgos. +\nAl confiar en este certificado aceptas estos riesgos. Sin animación Añadido a biblioteca Quitado de biblioteca @@ -302,9 +302,9 @@ Por capítulo más reciente Ver capítulos Cancelar todo - Colores claros - Colores oscuros - Colores del sistema + Claros + Oscuros + Del sistema Gestionar notificaciones Seguridad y privacidad Desbloqueo biométrico @@ -376,7 +376,7 @@ Datos Fuentes que faltan: La copia de seguridad no contiene ningun elemento; la biblioteca está vacía. - El archivo de copia de seguridad es inválido: + El archivo de copia de seguridad no parece estar bien: Comprueba si hay una nueva portada, información y descripción al actualizar la biblioteca Actualizar automáticamente los metadatos Cuadrícula amplia @@ -405,7 +405,7 @@ No se pudo descargar ningún capítulo, queda muy poco espacio Buscar por «%1$s» en todas las fuentes Modo de lectura - Tema de colores + Esquema de colores Por fecha en la biblioteca Todavía no has anclado ninguna fuente Terminadas @@ -554,22 +554,22 @@ Actualizaciones de la aplicación Limpiar la caché de capítulos al abrir la aplicación Base de datos limpia - %1$d entradas que no pertenecen a la biblioteca en la base de datos + %1$d títulos que no pertenecen a la biblioteca en la base de datos No se pudo descargar el listado de extensiones Política de privacidad - Omitir entradas con capítulos no leídos + Saltarse títulos con capítulos por leer Si necesitas ayuda para resolver los errores de actualización de la biblioteca mira en %1$s Guardar como archivo CBZ En pausa - Serie terminada + Publicación finalizada Cancelada Mostrar el elemento Desplazarse por el resto de la página antes de cambiar Cuadrícula sólo de portadas Acercar la vista en horizontal - Omitir entradas sin lectura iniciada + Saltarse títulos sin empezar Omitido, ya que su publicación ha terminado - Omitido porque hay capítulos sin leer + Omitido porque todavía hay capítulos sin leer Omitido porque no hay capítulos leídos Ver más detalles Actualizaciones fallidas: %1$d @@ -600,7 +600,7 @@ Clasificación por edades Leyendo A leer en un futuro - Manga terminado + Leídos del todo En pausa Sin terminar Solo en conexiones no medidas @@ -613,15 +613,15 @@ Borrar categoría ¿Quieres borrar la categoría «%s»\? ErrorInterno: Mira el registro de depuración para más información - User agent predeterminado - Restablecer user agent predeterminado + Identificarse como otro navegador web («user agent») + Volver a la identificación de navegador («user agent») original Quitar todo La app no soporta el formato RARv5 Aquí aparecerá el contenido más reciente de tu biblioteca El widget no está disponible cuando el bloqueo de aplicación está activo Ya se está actualizando Marea - El valor del user agent no puede estar en blanco + La cadena con el agente de usuario («user agent») no puede estar vacía Solo funciona si el capítulo actual y el que va después ya están descargados. Descargar por adelantado Descarga los capítulos siguientes mientras lees @@ -648,7 +648,7 @@ Título desconocido Ubicación incorrecta: %s Ahora mismo - Valor de user agent inválido + La cadena con el agente de usuario («user agent») no parece ser correcta Reindexando descargas Abrir un elemento al azar Parece que esta categoría está vacía @@ -662,7 +662,7 @@ Forzar a la aplicación a volver a comprobar los capítulos descargados Empezados En el dispositivo - Manga completado + Leídos del todo Tiempo de lectura Leídos Con seguimiento @@ -674,15 +674,15 @@ %dm En uso No disponible - Manga en servicios de seguimiento + En servicios de seguimiento Descargados Estadísticas Resumen - Manga + Títulos %ds Ahora no La categoría está vacía - Mostrar el número de capítulos por leer en el icono de actualizaciones + Ver número de capítulos por leer en el icono de actualizaciones Se ha copiado al portapapeles Saltarse los capítulos repetidos Está disponible, pero la fuente todavía no se ha instalado: %s @@ -698,7 +698,7 @@ Superposición Girar las páginas anchas para adaptarlas a la pantalla Girar las páginas anchas en la dirección opuesta - Información sobre la depuración + Información de depuración Deslizamiento de dedo en capítulos Deslizar a la izquierda Deslizar a la derecha @@ -706,7 +706,7 @@ %d por fila Tras 10 o más días Próxima actualización prevista - Prever la fecha del próximo lanzamiento + Intentar predecir cuándo sale el siguiente número Intervalos Ha pasado el período de comprobación Estimar cada @@ -770,15 +770,15 @@ ¿No es la primera vez que instalas %s? Saltar Siguiente - Lo primero de todo es poner las cosas a tu gusto. Siempre puedes volver a cambiarlas más tarde en los ajustes. - Todavía no has proporcionado una ubicación de almacenamiento + Lo primero de todo es dejar las cosas a tu gusto. Siempre puedes volver a cambiarlas más tarde en los ajustes. + Todavía no has proporcionado ninguna carpeta Selecciona una carpeta donde %1$s almacenará las descargas de capítulos, copias de seguridad y otras cosas. \n \nTe recomendamos que sea solo para %1$s. \n \nCarpeta seleccionada: %2$s Permiso para instalar aplicaciones - Permiso de notificación + Permiso para mostrar notificaciones Previene cortes y retrasos al procesar tareas en segundo plano que tarden un poco; como al buscar y descargar contenido nuevo, así como al restaurar copias de respaldo. Uso de batería en segundo plano Para instalar extensiones que te permiten buscar y descargar contenido. @@ -787,19 +787,21 @@ Disponible: %1$s / Total: %2$s Toca aquí para conceder los permisos necesarios para instalar extensiones. Incluir datos privados, como las claves de inicio de sesión en plataformas de seguimiento - Error completo: + Descripción completa del problema: Próxima actualización prevista en torno a %1$s, revisando cada %2$s - Esta extensión proviene de un repositorio externo. Toca para ver el repositorio. + Esta extensión proviene de un repositorio externo. Toca para verlo. Frecuencia de actualización personalizada: - ¡Este repositorio ya existe! - Actualización inteligente - URL de repositorio inválida - Agregar repositorios adicionales a Tachiyomi. La URL debe terminar con \"index.min.json\". - ¿Deseas eliminar el repositorio \"%s\"? - Eliminar repositorio + El repositorio ya existe + Actualizaciones inteligentes + La dirección URL del repositorio no parece ser correcta + Añade más repositorios a Tachiyomi; la dirección URL tiene que terminar en «index.min.json». + ¿Seguro que quieres borrar el repositorio «%s»? + Borrar repositorio Próxima actualización - Agregar repositorio - No tienes repositorios agregados. + Añadir un repositorio + Todavía no has añadido ningún repositorio. Repositorios de extensiones - URL del repositorio + Dirección URL del repositorio + ¿Acabas de actualizar desde una versión más antigua y no sabes qué hacer? Échale un vistazo a la guía de almacenamiento. + Guía de almacenamiento \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/fil/strings.xml b/i18n/src/commonMain/resources/MR/fil/strings.xml index 478ebe892..5d0d16314 100644 --- a/i18n/src/commonMain/resources/MR/fil/strings.xml +++ b/i18n/src/commonMain/resources/MR/fil/strings.xml @@ -180,11 +180,11 @@ Naka-fullscreen Wala sa opisyal na listahan ang extension na ito. Hindi na available ang extension na ito. Maaaring hindi ito gumana nang maayos at maaaring magdulot ng mga isyu sa app. Inirerekomenda ang pag-uninstall nito. - Pinirmahan ang extension na ito gamit ang isang kaduda-dudang certificate at hindi muna pinagana. + Ang extension na ito ay nilagdaan ng sinumang hindi kilalang author at hindi na-load. \n -\nMaaaring mabasa ng isang kaduda-dudang extension ang kahit anong nakatagong credentials sa pag-login o di kaya nama\'y magsimula ng delikadong code. +\nMaaaring basahin ng mga nakakahamak na extension ang anumang nakatagong kredensyal sa pag-log in o magsagawa ng arbitrary code. \n -\nTinatanggap mo ang mga bantang ito sa pagtiwala sa certificate na ito. +\nSa pamamagitan ng pagtitiwala sa certificate ng extension na ito, tinatanggap mo ang mga panganib na ito. Di-pinagkakatiwalaang extension I-uninstall Tiwala @@ -203,7 +203,7 @@ Default na kategorya Maghanap ng mga bagong cover at detalye kapag nag-a-update ng Aklatan Awtomatikong i-refresh ang metadata - May \"Kumpleto\" na estado + Laktawan ang mga entry na may katayuang \"Nakumpleto\" Kapag naka-charge Awtomatikong ina-update ang mga paghihigpit sa device Linggo-linggo @@ -223,8 +223,8 @@ Bantayan ang screen Baligtarin ang mga tap zone Nawawalang mga source: - Hindi naglalaman ang backup ng kahit anong mga entry sa Aklatan. - Invalid na backup + Hindi naglalaman ang backup ng kahit anong mga entry sa aklatan. + Imbalidong backup file: Nai-backup na Awtomatikong dalas ng pag-backup I-restore ang Aklatan mula sa backup @@ -389,7 +389,7 @@ Burahin ang nakaraan ng mga entry na hindi naka-save sa aklatan mo Linisin ang database Nagka-error habang nililinis - Nalinis na ang cache. Binura ang %1$d (na) file + Na-clear ang cache, na-delete ang %1$d na file Nagamit: %1$s Linisin ang cache ng kabanata Data @@ -457,9 +457,7 @@ Ipakita ang bilang ng mga item Kung sakaling hindi sumasakto sa direksyon ng pagbabasa ang paghahati sa malalapad na pahina Baligtarin ang paghahati sa pahina - Ire-restore ang mga datos mula sa backup file. -\n -\nKailangan mong i-install muli ang mga nawawalang extension at mag-login muli sa mga tracker pagkatapos para magamit ang mga ito. + Kailangan mong i-install muli ang mga nawawalang extension at mag-login muli sa mga tracker pagkatapos para magamit ang mga ito. DNS kesa HTTPS (DoH) Ang mga entry sa mga ibinukod na kategorya ay hindi mada-download kahit na sila ay kasama rin sa mga kategoryang kasama. Kusang pag-download @@ -491,7 +489,7 @@ Restriksyon: %s Walang nahanap na kapares Petsa - Invalid na format ng kabanata + Imbalidong format ng kabanata Hindi makita ang kabanata Di suportado ang source Di pa nabasa @@ -560,7 +558,7 @@ Bigong makuha ang listahan ng mga extension Patakaran sa Pagkapribado Para sa tulong sa pag-aayos ng mga error sa pag-update ng aklatan, tingnan ang %1$s - May di pa nababasang kabanata + Laktawan ang mga entry na hindi pa nababasang kabanata I-save bilang CBZ archive Tapos na\'ng mailathala Naka-hiatus @@ -568,7 +566,7 @@ Ipakita ang entry Pabalat lang Nilaktawan dahil kumpleto na ang serye - Hindi pa nasisimulan + Laktawan ang hindi nasimulang mga entry Nilaktawan dahil may di pa nabasang mga kabanata Nilaktawan dahil wala pang nabasang mga kabanata Awtomatikong mag-zoom sa mga malalawak na larawan @@ -649,7 +647,7 @@ Buksan muli ang app Invalid na lugar: %s Di alam na pamagat - Invalid na string ng user agent + Di-wastong string ng user agent Ngayon lang Tinitignan ang mga download Walang mga entry ang nahanap sa kategoryang ito @@ -715,7 +713,7 @@ Kunin kada buwan (kada ika-28 na araw) Tantyahin bawat Itakdang i-update bawat - Sa labas ng inaasahang release period + Hulaan ang susunod na oras ng release Mga pagitan Nilaktawan dahil walang inaasahang release ngayong araw May mga resulta @@ -746,7 +744,7 @@ Ang file picker ay nabigo na ibalik ang file sa app Data at storage Binabawasan ang ghosting sa mga e-ink na display - Mag-flash ng puti kada pagbabago ng pahina + Mag-flash kada pumalit ng pahina Hindi kailanman Huling awtomatikong na-back up: %s Paggamit ng storage @@ -769,7 +767,7 @@ Magsimula Dapat pumili ng isang folder Maligayang pagdating! - Gumagamit ba ng %s dati? + Muling pag-install ng %s? Laktawan Susunod Mag-set up muna tayo ng ilang bagay. Maaari mo ring baguhin ang mga ito anumang oras sa mga setting sa ibang pagkakataon. @@ -787,8 +785,23 @@ Maabisuhan para sa mga update sa aklatan at higit pa. Payagan Na magagamit: %1$s / Kabuuan: %2$s - Inaasahan ang susunod na update sa humigit-kumulang %s + Inaasahan ang susunod na update sa humigit-kumulang %1$s, na tumitingin sa bawat %2$s Buong error: Kinakailangan ng permiso para mag-install ng mga extension. I-tap upang mapayagan ito. Kasali ang mga sensitibong setting (hal., mga tracker login token) + Nag-a-update mula sa isang mas lumang bersyon at hindi sigurado kung ano ang pipiliin? Sumangguni sa gabay sa storage para sa higit pang impormasyon. + Matalas na pag-update + Wala kang na-set na repo. + Magdagdag ng repo + Mga repo ng extension + Gabay sa storage + Umiiral na ang repo na ito! + Tanggalin ang repo + URL ng repo + Magdagdag ng mga karagdagang repo sa Tachiyomi. Dapat ito ay isang URL na nagtatapos sa \"index.min.json\". + Ang extension na ito ay nagmula sa isang external na repo. I-tap para tignan ang repo. + Di-wastong URL ng repo + Susunod na update + Gusto mo bang tanggalin ang repo na \"%s\"? + Ipasadya ang update frequency: \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/fr/plurals.xml b/i18n/src/commonMain/resources/MR/fr/plurals.xml index e3c52a33d..9fc87129c 100644 --- a/i18n/src/commonMain/resources/MR/fr/plurals.xml +++ b/i18n/src/commonMain/resources/MR/fr/plurals.xml @@ -80,4 +80,9 @@ %d jours %d jours + + %d dépôt + %d dépôts + %d dépôts + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/fr/strings.xml b/i18n/src/commonMain/resources/MR/fr/strings.xml index a2a5dca3d..c7cd36e22 100644 --- a/i18n/src/commonMain/resources/MR/fr/strings.xml +++ b/i18n/src/commonMain/resources/MR/fr/strings.xml @@ -557,7 +557,7 @@ Rien à effacer Échec de la récupération de la liste des extensions Politique de confidentialité - Avec des chapitres non lus + Avec chapitre(s) non lu(s) Enregistrer comme archive CBZ Pour savoir comment corriger les erreurs de mise à jour de la bibliothèque, voir %1$s En pause @@ -709,7 +709,7 @@ Abandonné \? En retard de 20+ et 2 mois Période de contrôle réussie Prochaine mise à jour prévue - Période de diffusion prévue + Prochaine sortie prévue dans : Définir l\'intervalle Valider Intervalle de recherche personnalisé @@ -758,4 +758,21 @@ Des permissions sont nécessaires pour installer des extensions. Appuyer ici pour les accorder. Accorder Durées relatives + Permission de notifications + Utilisation de la batterie en arrière-plan + Pour installer des extensions de source. + Évite les interruptions des longues mises à jour de bibliothèque, téléchargements de chapitres et restaurations de sauvegardes. + Permission d\'installer des applications + Mise à jour depuis une ancienne version et pas sûr de quoi choisir ? Consultez le guide de stockage pour plus d\'informations. + Guide de stockage + Soyez notifié des mises à jour de la bibliothèque et autre. + Mise à jour intelligente + Guide de démarrage + Commençons par paramétrer certaines choses. Vous pouvez toujours changer ces paramètres plus tard. + Sélectionnez un dossier où %1$s stockera les chapitres téléchargés, les sauvegardes et plus encore. +\n +\nUn dossier dédié est recommandé. +\n +\nDossier sélectionné : %2$s + Sélectionnez un dossier \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/in/plurals.xml b/i18n/src/commonMain/resources/MR/in/plurals.xml index e661939a0..ff5428138 100644 --- a/i18n/src/commonMain/resources/MR/in/plurals.xml +++ b/i18n/src/commonMain/resources/MR/in/plurals.xml @@ -48,4 +48,7 @@ %d hari + + %d repo + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/in/strings.xml b/i18n/src/commonMain/resources/MR/in/strings.xml index 5da86361e..369df890f 100644 --- a/i18n/src/commonMain/resources/MR/in/strings.xml +++ b/i18n/src/commonMain/resources/MR/in/strings.xml @@ -78,7 +78,7 @@ Semua Pembatasan pembaruan otomatis Saat mengisi daya - Dengan status \"Selesai\" + Lewati daftar dengan status \"Selesai\" Perbarui kemajuan setelah membaca Kategori bawaan Selalu tanya @@ -241,11 +241,11 @@ Tidak terpercaya Lepas Ekstensi tidak terpercaya - Ekstensi ini ditandatangani dengan sertifikat tidak tepercaya dan tidak diaktifkan. + Ekstensi ini ditandatangani oleh author yang tidak dikenal dan tidak dimuat. \n -\nEkstensi berbahaya dapat membaca kredensial login apa pun yang disimpan atau mengeksekusi kode arbitrer. +\nEkstensi berbahaya dapat membaca semua kredensial login yang disimpan atau menjalankan kode berbahaya. \n -\nDengan mempercayai sertifikat ini, Anda menerima risiko ini. +\nDengan mempercayai sertifikat ekstensi ini, Anda menerima risiko ini. Kecepatan animasi ketukan dua kali Tanpa animasi Normal @@ -376,7 +376,7 @@ Data Sumber yang hilang: Cadangan tidak berisi entri perpustakaan apa pun. - Berkas cadangan tidak valid + File cadangan tidak valid: Menurut tanggal pengunggahan Grid nyaman Tab @@ -471,9 +471,7 @@ DNS melalui HTTPS (DoH) semacam Kindle Unduh otomatis - Data dari berkas cadangan akan dipulihkan. -\n -\nAnda perlu memasang ekstensi yang hilang dan masuk ke layanan pelacakan setelahnya untuk menggunakannya. + Anda mungkin perlu memasang ulang ekstensi yang hilang dan log in ke layanan pelacakan setelahnya untuk menggunakannya. Matikan mode penyamaran Sumber tidak didukung Tidak ditemukan kecocokan @@ -558,7 +556,7 @@ Tidak ada yang perlu dibersihkan %1$d entri non-perpustakaan dalam database Gagal mendapatkan daftar ekstensi - Dengan bab yang belum dibaca + Lewati daftar dengan chapter yang belum dibaca Kebijakan privasi Penerbitan selesai Dibatalkan @@ -567,7 +565,7 @@ Simpan sebagai arsip CBZ Lihat entri Grid sampul saja - Yang belum dibaca + Lewati dafter yang belum mulai dibaca Dilewati karena seri telah selesai Dilewati karena ada bab yang belum dibaca Dilewati karena tidak ada bab yang dibaca @@ -715,7 +713,7 @@ Melewati periode pemeriksaan Pembaruan yang diharapkan berikutnya Interval - Di luar periode rilis yang diharapkan + Prediksikan waktu rilis selanjutnya Perkirakan setiap Atur untuk memperbarui setiap Hapus %s pelacakan\? @@ -752,7 +750,7 @@ Terakhir dicadangkan secara otomatis: %s Tidak ditemukan pemindai Pemindai - Berkedip putih pada perubahan halaman + Perlihatkan kilatan saat halaman berubah Penggunaan penyimpanan Skor pelacak Data dan penyimpanan @@ -771,7 +769,7 @@ Izin notifikasi Izin pasang aplikasi Selamat Datang! - Sudah pernah menggunakan %s sebelumnya? + Install ulang %s? Lewati Hindari gangguan pada pembaruan pustaka, pengunduhan, dan pemulihan cadangan yang berlangsung lama. Selanjutnya @@ -786,4 +784,24 @@ \nSebaiknya menggunakan direktori terpisah. \n \nDirektori yang dipilih: %2$s + Panduan penyimpanan + Pembauan pintar + Memperbarui dari versi lama dan tak yakin harus pilih mana? lihat panduan penyimpanan untuk informasi lebih lanjut. + Tambahkan repo + Tambahkan repo lain ke Tachiyomi. Seharusnya URL yang memiliki akhiran \"index.min.json\". + Repositori ekstensi + Anda tidak memiliki repositori yang ditetapkan. + Keseluruhan eror: + Repositori ini sudah ada! + Update selanjutnya + URL Repo + URL repo tidak valid + Ekstensi ini dari repo eksternal. ketuk untuk lihat repo. + Sertakan pengaturan sensitif (contohnya, token login pelacak) + Izin diperlukan untuk memasang ekstensi. Klik disini untuk memberi izin. + Hapus repo + Apa Anda yakin ingin menghapus repo \"%s\"? + Update selanjutnya diperkirakan sekitar %1$s, memeriksa setiap sekitar %2$s + Tersedia:%1$s/Total:%2$s + Keseringan pembaruan yang di kostumisasi: \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ko/plurals.xml b/i18n/src/commonMain/resources/MR/ko/plurals.xml index 03c6fcff6..8ab39cfc2 100644 --- a/i18n/src/commonMain/resources/MR/ko/plurals.xml +++ b/i18n/src/commonMain/resources/MR/ko/plurals.xml @@ -48,4 +48,7 @@ %d일 + + %d 저장소 + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml b/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml index 5dfb1efc0..d76de753a 100644 --- a/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml +++ b/i18n/src/commonMain/resources/MR/nb-rNO/strings.xml @@ -767,7 +767,7 @@ Kom i gang En mappe må velges Velkommen! - Allerede brukt %s før? + Reinstallerer du %s? Hopp over Neste La oss sette opp noen ting først. Du kan alltid endre disse i innstillingene senere også. diff --git a/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml b/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml index 015b6bcc3..35a295861 100644 --- a/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/i18n/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -766,7 +766,7 @@ Novo no %s? Recomendamos dar uma olhada no guia de introdução. Começar Bem-vindo(a)! - Já utilizou o %s antes? + Reinstalando o %s? Pular Próximo Vamos definir algumas coisas primeiro. Você sempre pode fazer alterações nas configurações depois também. @@ -793,12 +793,15 @@ Erro completo: Este repositório já existe! Atualização inteligente - Nome de repositório inválido - Adiciona repositórios adicionais ao Tachiyomi. O formato de um repositório é \"usuário/repositório\", onde \"usuário\" é o dono do repositório e \"repositório\" é o nome do repositório. + URL do repositório inválido + Adiciona repositórios adicionais ao Tachiyomi. Deve ser uma URL que termine com \"index.min.json\". Você deseja deletar o repositório \"%s\"? Deletar repositório Próxima atualização Adicionar repositório Você não tem repositórios definidos. Repositórios de extensões + URL do repositório + Guia de armazenamento + Atualizando de uma versão anterior e não tem certeza do que selecionar? Consulte o guia de armazenamento para mais informações. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/pt/strings.xml b/i18n/src/commonMain/resources/MR/pt/strings.xml index 3846adafb..eba95f695 100644 --- a/i18n/src/commonMain/resources/MR/pt/strings.xml +++ b/i18n/src/commonMain/resources/MR/pt/strings.xml @@ -716,4 +716,10 @@ Definido para atualizar a cada Pulado, pois nenhum lançamento é esperado para hoje Deletar dowloand + Selecionado + Não selecionado + Mais opções + Rolar para cima + Desbloquear %s + Dados e armazenamento \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/ru/strings.xml b/i18n/src/commonMain/resources/MR/ru/strings.xml index 26971658d..99ecf88fe 100644 --- a/i18n/src/commonMain/resources/MR/ru/strings.xml +++ b/i18n/src/commonMain/resources/MR/ru/strings.xml @@ -767,7 +767,7 @@ Начать Необходимо выбрать папку Добро пожаловать! - Уже использовали %s раньше? + Переустанавливаете %s? Пропустить Следующее Давайте настроем парочку вещей. Вы всегда можете их поменять позже в настройках. @@ -802,4 +802,6 @@ У вас нет репозиториев. Репозитории расширений URL-адрес репозитория + Руководство по хранению + Обновляетесь со старой версии и не знаете, что выбрать? Обратите внимание на руководство по хранению для большей информации. \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml b/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml index 3b2ec7005..c6b0f9571 100644 --- a/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml +++ b/i18n/src/commonMain/resources/MR/zh-rCN/plurals.xml @@ -48,4 +48,7 @@ %d 天 + + %d 仓库 + \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml b/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml index a3c42d053..d8ea9faa2 100644 --- a/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/i18n/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -83,7 +83,7 @@ 全部 设备自动更新限制 充电时 - 已完结 + 跳过状态为“已完结”的作品 阅读后更新进度 默认分类 始终询问 @@ -97,11 +97,11 @@ 不可信 卸载 不可信的插件 - 此插件使用不受信任的证书签名且尚未激活。 + 此插件由未知作者签名且尚未加载。 \n \n恶意插件可以读取任何存储的登录凭据或执行任意代码。 \n -\n信任此证书即代表你愿意承担上述风险。 +\n信任此插件证书即代表你愿意承担上述风险。 全屏 页面过渡动画 双击动画速度 @@ -167,7 +167,7 @@ 正在创建备份 清除章节缓存 已使用空间:%1$s - 缓存已清除。%1$d 个文件已被删除 + 缓存已清除,%1$d个文件已被删除 清除时出现错误 清除 Cookie Cookie 已清除 @@ -301,8 +301,8 @@ 作品更新时间 查看章节 全部取消 - 关闭 - 开启 + 浅色 + 深色 跟随系统 通知管理 隐私 @@ -376,7 +376,7 @@ 数据 缺少图源: 备份不包含任何作品。 - 无效的备份文件 + 无效的备份文件: 更新书架时一并检查封面和简介是否有变动 自动刷新元数据 迁移 @@ -457,9 +457,7 @@ 拆分双页 拆分双页时交换顺序 拆分双页的顺序与阅读方向不符时可以开启 - 即将还原备份文件中的数据。 -\n -\n随后请重新安装所有缺失的插件并重新登录进度记录平台。 + 您可能需要安装缺失的插件并登录进度记录平台才能正常使用。 下一页 @@ -559,7 +557,7 @@ 数据库中有 %1$d 部作品未添加到书架 无法获取插件列表 隐私声明 - 有未读章节 + 跳过有未读章节的作品 关于书架更新出错的解决方法,请参阅 %1$s 保存为 CBZ 压缩包 出版完毕 @@ -569,7 +567,7 @@ 封面网格 自动放大横向图片 图片放大时先平移再翻页 - 尚未开始阅读 + 跳过还未开始阅读的作品 已跳过,因为作品已完结 已跳过,因为有未读章节 已跳过,因为尚未开始阅读 @@ -579,8 +577,8 @@ 将作品移到顶部 关闭 保存图片出错 - 新版本可从官方版本中获得。 轻按以了解如何从非官方 F-Droid 版本迁移。 - 没有要备份的库条目 + 官方发布了新版本。 轻按以了解如何从非官方 F-Droid 版本迁移。 + 书架没有可备份的作品 打开 GitHub 页面 已清除 WebView 数据 清除 WebView 数据 @@ -706,7 +704,7 @@ 向左滑动操作 双击放大 每行 %d 个 - 未到预计更新时间 + 预计下次更新时间 确定 要删除 %s 的记录吗? 同时删除 %s 上的数据 @@ -747,7 +745,7 @@ 从不 减少电子墨水屏上的重影 上次自动备份:%s - 切页时闪烁白屏 + 切页时闪烁 数据与存储 存储占用 创建 @@ -766,5 +764,44 @@ 跳过 下一步 向上浏览 - 让我们先设置一些东西。你也可以稍后在设置中更改这些。 + 让我们先进行一些设置。您也可以稍后在设置中更改这些选项。 + 避免长时间运行的书架更新、下载和备份恢复被中断。 + 授权 + 未设置存储位置 + 完整错误: + 后台电池配置 + 需要权限安装插件。点击此处授权。 + 添加仓库 + 包含敏感设置(例如进度记录平台的登录令牌) + 您希望删除仓库\"%s\"吗? + 此插件来自外部仓库。点击查看该仓库。 + 此仓库已存在! + 删除仓库 + 无效的仓库URL + 用于自动备份、章节下载和本地图源。 + 下次更新预计在%1$s天后,每隔%2$s检查一次 + 自定义更新频率: + 应用安装权限 + 用以安装图源插件。 + 通知权限 + %s新手?我们建议您查阅入门指南。 + 重新安装%s? + 智能更新 + 获取书架更新及其他通知。 + 插件仓库 + 您没有设置仓库。 + 仓库URL + 向 Tachiyomi 添加额外仓库。注意是以“index.min.json”结尾的 URL。 + 下次更新 + 从旧版本升级而来不知如何选择?请参阅存储指南获取更多信息。 + 存储指南 + 存储位置 + 选择一个文件夹 + 选择文件夹%1$s用以存储下载章节、备份文件及其他。 +\n +\n推荐使用专用文件夹。 +\n +\n已选文件夹:%2$s + 必须选择一个文件夹 + 可用:%1$s / 全部:%2$s \ No newline at end of file diff --git a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml b/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml index 70e7baa0f..568846942 100644 --- a/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/i18n/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -80,7 +80,7 @@ 全部 自動更新的裝置限制 充電時 - 連載已完結 + 略過已完結的作品 預設類別 總是詢問 更新 @@ -557,7 +557,7 @@ 無須清理 擴充套件清單取得失敗 隱私權政策 - 有未讀章節 + 略過有未讀章節的作品 如欲瞭解如何修復書櫃更新錯誤,請參閱 %1$s 以 CBZ 封存檔格式儲存 休刊中 @@ -567,7 +567,7 @@ 自動縮放寬頁 導覽寬頁時先平移後翻頁 純封面格狀 - 無已讀章節 + 略過無已讀章節的作品 由於連載已完結,因此略過 由於有未讀章節,因此略過 由於無已讀章節,因此略過 @@ -718,7 +718,7 @@ 下次預期更新 刊期 預計每個 - 未臨出刊日 + 預估出刊日 設定為每個 由於未臨出刊日,因此略過 有結果 @@ -767,7 +767,7 @@ 開始使用 必須選擇一個資料夾 歡迎! - 已是 %s 的既有使用者? + 重新安裝 %s? 略過 下一步 讓我們先設定一些東西。稍後你隨時可至設定中變更這些選項。 @@ -787,7 +787,7 @@ 可用:%1$s / 總計:%2$s 缺少安裝擴充套件所需的權限。輕觸此處以授予。 包含敏感設定 (如:歷程平台登入權杖) - 預計再 %s左右出刊 + 預計 %1$s後出刊,每 %2$s檢查一次 完整錯誤訊息: 此擴充套件來自外部儲存庫,輕觸以檢視該儲存庫。 自訂更新頻率: @@ -801,4 +801,6 @@ 新增儲存庫 尚未新增任何儲存庫 擴充套件儲存庫 + 儲存空間指南 + 從舊版升級而不確定該如何選擇嗎?請參閱儲存空間指南以取得更多資訊。 \ No newline at end of file