diff --git a/.editorconfig b/.editorconfig index bbef1d752..d1f195728 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,5 +3,5 @@ indent_size=4 insert_final_newline=true ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma_on_call_site=true -ij_kotlin_name_count_to_use_star_import = 2147483647 -ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 \ No newline at end of file +ij_kotlin_name_count_to_use_star_import=2147483647 +ij_kotlin_name_count_to_use_star_import_for_members=2147483647 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 0d9d67163..3e311f12f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,7 +5,7 @@ I acknowledge that: - I have updated: - To the latest version of the app (stable is v0.14.6) - All extensions -- I have gone through the FAQ (https://tachiyomi.org/help/faq/) and troubleshooting guide (https://tachiyomi.org/help/guides/troubleshooting/) +- 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 - I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue - I will fill out the title and the information in this template diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0922c459d..dddf1e374 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,8 +4,8 @@ contact_links: url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead - name: 📦 Tachiyomi extensions - url: https://tachiyomi.org/extensions + url: https://tachiyomi.org/extensions/ about: List of all available extensions with download links - name: 🖥️ Tachiyomi website - url: https://tachiyomi.org/help/ + url: https://tachiyomi.org/ about: Guides, troubleshooting, and answers to common questions diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index 460e43d82..e80993914 100644 --- a/.github/ISSUE_TEMPLATE/report_issue.yml +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -96,7 +96,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 gone through the [FAQ](https://tachiyomi.org/help/faq/) and [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/). + - 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.14.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. required: true diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index a9af6d308..da79440dc 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 @@ -36,4 +36,4 @@ jobs: - name: Build app and run unit tests uses: gradle/gradle-command-action@v2 with: - arguments: lintKotlin assembleStandardRelease testReleaseUnitTest \ No newline at end of file + arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest \ No newline at end of file diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index e49ef19cb..e43c229a0 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Clone repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 @@ -31,7 +31,7 @@ jobs: - name: Build app and run unit tests uses: gradle/gradle-command-action@v2 with: - arguments: lintKotlin assembleStandardRelease testReleaseUnitTest + arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest # Sign APK and create release for tags @@ -104,3 +104,13 @@ jobs: prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + update-website: + needs: [build] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi' + steps: + - name: Trigger Netlify build hook + run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}" + env: + TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }} diff --git a/.github/workflows/issue_moderator.yml b/.github/workflows/issue_moderator.yml index 8c88132d0..8e937956c 100644 --- a/.github/workflows/issue_moderator.yml +++ b/.github/workflows/issue_moderator.yml @@ -39,7 +39,7 @@ jobs: "regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?Issues -1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).** +1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).** 2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a7d394f99..78216f47f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,4 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jmailen.gradle.kotlinter.tasks.LintTask plugins { id("com.android.application") @@ -23,7 +22,7 @@ android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 105 + versionCode = 107 versionName = "0.14.6" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") @@ -104,15 +103,17 @@ android { } packaging { - resources.excludes.addAll(listOf( - "META-INF/DEPENDENCIES", - "LICENSE.txt", - "META-INF/LICENSE", - "META-INF/LICENSE.txt", - "META-INF/README.md", - "META-INF/NOTICE", - "META-INF/*.kotlin_module", - )) + resources.excludes.addAll( + listOf( + "META-INF/DEPENDENCIES", + "LICENSE.txt", + "META-INF/LICENSE", + "META-INF/LICENSE.txt", + "META-INF/README.md", + "META-INF/NOTICE", + "META-INF/*.kotlin_module", + ), + ) } dependenciesInfo { @@ -239,7 +240,6 @@ dependencies { implementation(libs.aboutLibraries.compose) implementation(libs.bundles.voyager) implementation(libs.compose.materialmotion) - implementation(libs.compose.simpleicons) implementation(libs.swipe) // Logging @@ -267,7 +267,9 @@ androidComponents { beforeVariants { variantBuilder -> // Disables standardBenchmark if (variantBuilder.buildType == "benchmark") { - variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev")) + variantBuilder.enable = variantBuilder.productFlavors.containsAll( + listOf("default" to "dev"), + ) } } onVariants(selector().withFlavor("default" to "standard")) { @@ -278,10 +280,6 @@ androidComponents { } tasks { - withType().configureEach { - exclude { it.file.path.contains("generated[\\\\/]".toRegex()) } - } - // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) withType { kotlinOptions.freeCompilerArgs += listOf( @@ -306,12 +304,12 @@ tasks { kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + - project.buildDir.absolutePath + "/compose_metrics" + project.buildDir.absolutePath + "/compose_metrics", ) kotlinOptions.freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + - project.buildDir.absolutePath + "/compose_metrics" + project.buildDir.absolutePath + "/compose_metrics", ) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f75bf4b8c..424952c7d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -155,20 +155,6 @@ android:name=".data.notification.NotificationReceiver" android:exported="false" /> - - - - - - - - diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 494033c87..4da261de6 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -1,7 +1,6 @@ package eu.kanade.domain import eu.kanade.domain.chapter.interactor.SetReadStatus -import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.download.interactor.DeleteDownload import eu.kanade.domain.extension.interactor.GetExtensionLanguages @@ -16,7 +15,9 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSourcePin +import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.RefreshTracks +import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.interactor.TrackChapter import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.chapter.ChapterRepositoryImpl @@ -50,6 +51,7 @@ import tachiyomi.domain.history.interactor.GetTotalReadDuration import tachiyomi.domain.history.interactor.RemoveHistory import tachiyomi.domain.history.interactor.UpsertHistory import tachiyomi.domain.history.repository.HistoryRepository +import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetLibraryManga @@ -57,7 +59,6 @@ import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetMangaWithChapters import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.ResetViewerFlags -import tachiyomi.domain.manga.interactor.SetFetchInterval import tachiyomi.domain.manga.interactor.SetMangaChapterFlags import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.release.interactor.GetApplicationRelease @@ -102,7 +103,7 @@ class DomainModule : InjektModule { addFactory { GetNextChapters(get(), get(), get()) } addFactory { ResetViewerFlags(get()) } addFactory { SetMangaChapterFlags(get()) } - addFactory { SetFetchInterval(get()) } + addFactory { FetchInterval(get()) } addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) } addFactory { SetMangaViewerFlags(get()) } addFactory { NetworkToLocalManga(get()) } @@ -114,11 +115,13 @@ class DomainModule : InjektModule { addSingletonFactory { TrackRepositoryImpl(get()) } addFactory { TrackChapter(get(), get(), get(), get()) } + addFactory { AddTracks(get(), get(), get()) } addFactory { RefreshTracks(get(), get(), get(), get()) } addFactory { DeleteTrack(get()) } addFactory { GetTracksPerManga(get()) } addFactory { GetTracks(get()) } addFactory { InsertTrack(get()) } + addFactory { SyncChapterProgressWithTrack(get(), get(), get()) } addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { GetChapter(get()) } @@ -127,7 +130,6 @@ class DomainModule : InjektModule { addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { ShouldUpdateDbChapter() } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) } - addFactory { SyncChapterProgressWithTrack(get(), get(), get()) } addSingletonFactory { HistoryRepositoryImpl(get()) } addFactory { GetHistory(get()) } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt index 38d6083ff..468ea2389 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt @@ -3,7 +3,7 @@ package eu.kanade.domain.manga.interactor import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.model.SManga -import tachiyomi.domain.manga.interactor.SetFetchInterval +import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.repository.MangaRepository @@ -15,7 +15,7 @@ import java.util.Date class UpdateManga( private val mangaRepository: MangaRepository, - private val setFetchInterval: SetFetchInterval, + private val fetchInterval: FetchInterval, ) { suspend fun await(mangaUpdate: MangaUpdate): Boolean { @@ -79,9 +79,9 @@ class UpdateManga( suspend fun awaitUpdateFetchInterval( manga: Manga, dateTime: ZonedDateTime = ZonedDateTime.now(), - window: Pair = setFetchInterval.getWindow(dateTime), + window: Pair = fetchInterval.getWindow(dateTime), ): Boolean { - return setFetchInterval.toMangaUpdateOrNull(manga, dateTime, window) + return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window) ?.let { mangaRepository.update(it) } ?: false } diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt new file mode 100644 index 000000000..45340e44a --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt @@ -0,0 +1,45 @@ +package eu.kanade.domain.track.interactor + +import eu.kanade.domain.track.model.toDomainTrack +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.Tracker +import eu.kanade.tachiyomi.source.Source +import logcat.LogPriority +import tachiyomi.core.util.lang.withNonCancellableContext +import tachiyomi.core.util.system.logcat +import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.track.interactor.GetTracks +import tachiyomi.domain.track.interactor.InsertTrack + +class AddTracks( + private val getTracks: GetTracks, + private val insertTrack: InsertTrack, + private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, +) { + + suspend fun bindEnhancedTracks(manga: Manga, source: Source) = withNonCancellableContext { + getTracks.await(manga.id) + .filterIsInstance() + .filter { it.accept(source) } + .forEach { service -> + try { + service.match(manga)?.let { track -> + track.manga_id = manga.id + (service as Tracker).bind(track) + insertTrack.await(track.toDomainTrack()!!) + + syncChapterProgressWithTrack.await( + manga.id, + track.toDomainTrack()!!, + service, + ) + } + } catch (e: Exception) { + logcat( + LogPriority.WARN, + e, + ) { "Could not match manga: ${manga.title} with service $service" } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt index 87b7c8d99..8c8952304 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt @@ -1,10 +1,9 @@ package eu.kanade.domain.track.interactor -import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDomainTrack -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.Tracker +import eu.kanade.tachiyomi.data.track.TrackerManager import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.supervisorScope @@ -13,7 +12,7 @@ import tachiyomi.domain.track.interactor.InsertTrack class RefreshTracks( private val getTracks: GetTracks, - private val trackManager: TrackManager, + private val trackerManager: TrackerManager, private val insertTrack: InsertTrack, private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, ) { @@ -23,18 +22,17 @@ class RefreshTracks( * * @return Failed updates. */ - suspend fun await(mangaId: Long): List> { + suspend fun await(mangaId: Long): List> { return supervisorScope { return@supervisorScope getTracks.await(mangaId) - .map { track -> + .map { it to trackerManager.get(it.syncId) } + .filter { (_, service) -> service?.isLoggedIn == true } + .map { (track, service) -> async { - val service = trackManager.getService(track.syncId) return@async try { - if (service?.isLoggedIn == true) { - val updatedTrack = service.refresh(track.toDbTrack()) - insertTrack.await(updatedTrack.toDomainTrack()!!) - syncChapterProgressWithTrack.await(mangaId, track, service) - } + val updatedTrack = service!!.refresh(track.toDbTrack()) + insertTrack.await(updatedTrack.toDomainTrack()!!) + syncChapterProgressWithTrack.await(mangaId, track, service) null } catch (e: Throwable) { service to e diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChapterProgressWithTrack.kt b/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt similarity index 83% rename from app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChapterProgressWithTrack.kt rename to app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt index 86862504c..dcb95ff26 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChapterProgressWithTrack.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/SyncChapterProgressWithTrack.kt @@ -1,8 +1,8 @@ -package eu.kanade.domain.chapter.interactor +package eu.kanade.domain.track.interactor import eu.kanade.domain.track.model.toDbTrack -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.Tracker import logcat.LogPriority import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.interactor.GetChapterByMangaId @@ -20,9 +20,9 @@ class SyncChapterProgressWithTrack( suspend fun await( mangaId: Long, remoteTrack: Track, - service: TrackService, + tracker: Tracker, ) { - if (service !is EnhancedTrackService) { + if (tracker !is EnhancedTracker) { return } @@ -39,7 +39,7 @@ class SyncChapterProgressWithTrack( val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble()) try { - service.update(updatedTrack.toDbTrack()) + tracker.update(updatedTrack.toDbTrack()) updateChapter.awaitAll(chapterUpdates) insertTrack.await(updatedTrack) } catch (e: Throwable) { diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt b/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt index 96f2f6ca4..fa6245f22 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt @@ -4,30 +4,29 @@ import android.content.Context import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.service.DelayedTrackingUpdateJob import eu.kanade.domain.track.store.DelayedTrackingStore -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import logcat.LogPriority -import tachiyomi.core.util.lang.launchNonCancellable +import tachiyomi.core.util.lang.withNonCancellableContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.InsertTrack class TrackChapter( private val getTracks: GetTracks, - private val trackManager: TrackManager, + private val trackerManager: TrackerManager, private val insertTrack: InsertTrack, private val delayedTrackingStore: DelayedTrackingStore, ) { - suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) = coroutineScope { - launchNonCancellable { + suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) { + withNonCancellableContext { val tracks = getTracks.await(mangaId) - if (tracks.isEmpty()) return@launchNonCancellable + if (tracks.isEmpty()) return@withNonCancellableContext tracks.mapNotNull { track -> - val service = trackManager.getService(track.syncId) + val service = trackerManager.get(track.syncId) if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) { return@mapNotNull null } diff --git a/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt b/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt index 0273e0fdc..f578bd600 100644 --- a/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt +++ b/app/src/main/java/eu/kanade/domain/track/service/DelayedTrackingUpdateJob.kt @@ -10,7 +10,7 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkerParameters import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.store.DelayedTrackingStore -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.util.system.workManager import logcat.LogPriority import tachiyomi.core.util.lang.withIOContext @@ -33,7 +33,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) val getTracks = Injekt.get() val insertTrack = Injekt.get() - val trackManager = Injekt.get() + val trackerManager = Injekt.get() val delayedTrackingStore = Injekt.get() withIOContext { @@ -47,7 +47,7 @@ class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) } .forEach { track -> try { - val service = trackManager.getService(track.syncId) + val service = trackerManager.get(track.syncId) if (service != null && service.isLoggedIn) { logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" } service.update(track.toDbTrack(), true) diff --git a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt index 2ddba51e0..c7fb47581 100644 --- a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt @@ -1,33 +1,34 @@ package eu.kanade.domain.track.service -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.anilist.Anilist +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore class TrackPreferences( private val preferenceStore: PreferenceStore, ) { - fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "") + fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "") - fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "") + fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "") - fun setTrackCredentials(sync: TrackService, username: String, password: String) { + fun setCredentials(sync: Tracker, username: String, password: String) { trackUsername(sync).set(username) trackPassword(sync).set(password) } - fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "") + fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "") fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true) companion object { - fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId" + fun trackUsername(syncId: Long) = Preference.privateKey("pref_mangasync_username_$syncId") - private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId" + private fun trackPassword(syncId: Long) = Preference.privateKey("pref_mangasync_password_$syncId") - private fun trackToken(syncId: Long) = "track_token_$syncId" + private fun trackToken(syncId: Long) = Preference.privateKey("track_token_$syncId") } } diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index ac04a51e0..294812bdc 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -28,6 +28,8 @@ class UiPreferences( fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) + fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true) + fun dateFormat() = preferenceStore.getString("app_date_format", "") fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) diff --git a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt index f7d699164..3e3364d79 100644 --- a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt @@ -7,13 +7,22 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.SortByAlpha +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryListItem import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.category.CategoryScreenState import tachiyomi.domain.category.model.Category @@ -27,6 +36,7 @@ import tachiyomi.presentation.core.util.plus fun CategoryScreen( state: CategoryScreenState.Success, onClickCreate: () -> Unit, + onClickSortAlphabetically: () -> Unit, onClickRename: (Category) -> Unit, onClickDelete: (Category) -> Unit, onClickMoveUp: (Category) -> Unit, @@ -36,9 +46,32 @@ fun CategoryScreen( val lazyListState = rememberLazyListState() Scaffold( topBar = { scrollBehavior -> - AppBar( - title = stringResource(R.string.action_edit_categories), - navigateUp = navigateUp, + TopAppBar( + title = { + Text( + text = stringResource(R.string.action_edit_categories), + modifier = Modifier.padding(start = 8.dp), + ) + }, + navigationIcon = { + IconButton(onClick = navigateUp) { + Icon( + imageVector = Icons.Outlined.ArrowBack, + contentDescription = stringResource(R.string.abc_action_bar_up_description), + ) + } + }, + actions = { + AppBarActions( + listOf( + AppBar.Action( + title = stringResource(R.string.action_sort), + icon = Icons.Outlined.SortByAlpha, + onClick = onClickSortAlphabetically, + ), + ), + ) + }, scrollBehavior = scrollBehavior, ) }, diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt index ce0f59a7e..ad30e4e2d 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt @@ -162,7 +162,7 @@ fun CategoryDeleteDialog( TextButton(onClick = { onDelete() onDismissRequest() - },) { + }) { Text(text = stringResource(R.string.action_ok)) } }, @@ -180,6 +180,35 @@ fun CategoryDeleteDialog( ) } +@Composable +fun CategorySortAlphabeticallyDialog( + onDismissRequest: () -> Unit, + onSort: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = { + onSort() + onDismissRequest() + }) { + Text(text = stringResource(R.string.action_ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(R.string.action_cancel)) + } + }, + title = { + Text(text = stringResource(R.string.action_sort_category)) + }, + text = { + Text(text = stringResource(R.string.sort_category_confirmation)) + }, + ) +} + @Composable fun ChangeCategoryDialog( initialSelection: List>, @@ -217,7 +246,7 @@ fun ChangeCategoryDialog( tachiyomi.presentation.core.components.material.TextButton(onClick = { onDismissRequest() onEditCategories() - },) { + }) { Text(text = stringResource(R.string.action_edit)) } Spacer(modifier = Modifier.weight(1f)) diff --git a/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt b/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt index 89bb61b4f..2466e2eed 100644 --- a/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/components/RelativeDateHeader.kt @@ -13,13 +13,18 @@ import java.util.Date fun RelativeDateHeader( modifier: Modifier = Modifier, date: Date, + relativeTime: Boolean, dateFormat: DateFormat, ) { val context = LocalContext.current ListGroupHeader( modifier = modifier, text = remember { - date.toRelativeString(context, dateFormat) + date.toRelativeString( + context, + relativeTime, + dateFormat, + ) }, ) } diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index 3bac670d0..45dc67fdb 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -27,7 +27,6 @@ import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.text.DateFormat import java.util.Date @Composable @@ -98,7 +97,8 @@ private fun HistoryScreenContent( onClickDelete: (HistoryWithRelations) -> Unit, preferences: UiPreferences = Injekt.get(), ) { - val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } + val relativeTime = remember { preferences.relativeTime().get() } + val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } FastScrollLazyColumn( contentPadding = contentPadding, @@ -118,6 +118,7 @@ private fun HistoryScreenContent( RelativeDateHeader( modifier = Modifier.animateItemPlacement(), date = item.date, + relativeTime = relativeTime, dateFormat = dateFormat, ) } diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt index 55cd0aac4..12db68602 100644 --- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialogs.kt @@ -61,7 +61,7 @@ fun HistoryDeleteDialog( TextButton(onClick = { onDelete(removeEverything) onDismissRequest() - },) { + }) { Text(text = stringResource(R.string.action_remove)) } }, @@ -90,7 +90,7 @@ fun HistoryDeleteAllDialog( TextButton(onClick = { onDelete() onDismissRequest() - },) { + }) { Text(text = stringResource(R.string.action_ok)) } }, diff --git a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt index 0c8a80217..6d7882f43 100644 --- a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt @@ -108,13 +108,13 @@ private fun ColumnScope.FilterPage( onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) }, ) - val trackServices = remember { screenModel.trackServices } - when (trackServices.size) { + val trackers = remember { screenModel.trackers } + when (trackers.size) { 0 -> { // No trackers } 1 -> { - val service = trackServices[0] + val service = trackers[0] val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() TriStateItem( label = stringResource(R.string.action_filter_tracked), @@ -124,7 +124,7 @@ private fun ColumnScope.FilterPage( } else -> { HeadingItem(R.string.action_filter_tracked) - trackServices.map { service -> + trackers.map { service -> val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() TriStateItem( label = service.name, diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 740ece4a2..82c266f65 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -85,6 +85,7 @@ fun MangaScreen( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, fetchInterval: Int?, + dateRelativeTime: Boolean, dateFormat: DateFormat, isTabletUi: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, @@ -140,6 +141,7 @@ fun MangaScreen( MangaScreenSmallImpl( state = state, snackbarHostState = snackbarHostState, + dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, fetchInterval = fetchInterval, chapterSwipeStartAction = chapterSwipeStartAction, @@ -176,6 +178,7 @@ fun MangaScreen( MangaScreenLargeImpl( state = state, snackbarHostState = snackbarHostState, + dateRelativeTime = dateRelativeTime, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, dateFormat = dateFormat, @@ -215,6 +218,7 @@ fun MangaScreen( private fun MangaScreenSmallImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, + dateRelativeTime: Boolean, dateFormat: DateFormat, fetchInterval: Int?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, @@ -264,8 +268,14 @@ private fun MangaScreenSmallImpl( val chapters = remember(state) { state.processedChapters } + val isAnySelected by remember { + derivedStateOf { + chapters.fastAny { it.selected } + } + } + val internalOnBackPressed = { - if (chapters.fastAny { it.selected }) { + if (isAnySelected) { onAllChapterSelected(false) } else { onBackClicked() @@ -275,19 +285,22 @@ private fun MangaScreenSmallImpl( Scaffold( topBar = { - val firstVisibleItemIndex by remember { - derivedStateOf { chapterListState.firstVisibleItemIndex } + val selectedChapterCount: Int = remember(chapters) { + chapters.count { it.selected } } - val firstVisibleItemScrollOffset by remember { - derivedStateOf { chapterListState.firstVisibleItemScrollOffset } + val isFirstItemVisible by remember { + derivedStateOf { chapterListState.firstVisibleItemIndex == 0 } + } + val isFirstItemScrolled by remember { + derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 } } val animatedTitleAlpha by animateFloatAsState( - if (firstVisibleItemIndex > 0) 1f else 0f, - label = "titleAlpha", + if (!isFirstItemVisible) 1f else 0f, + label = "Top Bar Title", ) val animatedBgAlpha by animateFloatAsState( - if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f, - label = "bgAlpha", + if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f, + label = "Top Bar Background", ) MangaToolbar( title = state.manga.title, @@ -301,14 +314,17 @@ private fun MangaScreenSmallImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, - actionModeCounter = chapters.count { it.selected }, + actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, ) }, bottomBar = { + val selectedChapters = remember(chapters) { + chapters.filter { it.selected } + } SharedMangaBottomActionMenu( - selected = chapters.filter { it.selected }, + selected = selectedChapters, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -319,19 +335,20 @@ private fun MangaScreenSmallImpl( }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { + val isFABVisible = remember(chapters) { + chapters.fastAny { !it.chapter.read } && !isAnySelected + } AnimatedVisibility( - visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected }, + visible = isFABVisible, enter = fadeIn(), exit = fadeOut(), ) { ExtendedFloatingActionButton( text = { - val id = if (state.chapters.fastAny { it.chapter.read }) { - R.string.action_resume - } else { - R.string.action_start + val isReading = remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } } - Text(text = stringResource(id)) + Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start)) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, @@ -345,7 +362,7 @@ private fun MangaScreenSmallImpl( PullRefresh( refreshing = state.isRefreshingData, onRefresh = onRefresh, - enabled = chapters.fastAll { !it.selected }, + enabled = !isAnySelected, indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(), ) { val layoutDirection = LocalLayoutDirection.current @@ -417,10 +434,13 @@ private fun MangaScreenSmallImpl( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, ) { + val missingChapterCount = remember(chapters) { + chapters.map { it.chapter.chapterNumber }.missingChaptersCount() + } ChapterHeader( - enabled = chapters.fastAll { !it.selected }, + enabled = !isAnySelected, chapterCount = chapters.size, - missingChapterCount = chapters.map { it.chapter.chapterNumber }.missingChaptersCount(), + missingChapterCount = missingChapterCount, onClick = onFilterClicked, ) } @@ -428,6 +448,7 @@ private fun MangaScreenSmallImpl( sharedChapterItems( manga = state.manga, chapters = chapters, + dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, @@ -446,6 +467,7 @@ private fun MangaScreenSmallImpl( fun MangaScreenLargeImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, + dateRelativeTime: Boolean, dateFormat: DateFormat, fetchInterval: Int?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, @@ -496,12 +518,18 @@ fun MangaScreenLargeImpl( val chapters = remember(state) { state.processedChapters } + val isAnySelected by remember { + derivedStateOf { + chapters.fastAny { it.selected } + } + } + val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() var topBarHeight by remember { mutableIntStateOf(0) } PullRefresh( refreshing = state.isRefreshingData, onRefresh = onRefresh, - enabled = chapters.fastAll { !it.selected }, + enabled = !isAnySelected, indicatorPadding = PaddingValues( start = insetPadding.calculateStartPadding(layoutDirection), top = with(density) { topBarHeight.toDp() }, @@ -511,7 +539,7 @@ fun MangaScreenLargeImpl( val chapterListState = rememberLazyListState() val internalOnBackPressed = { - if (chapters.fastAny { it.selected }) { + if (isAnySelected) { onAllChapterSelected(false) } else { onBackClicked() @@ -521,10 +549,13 @@ fun MangaScreenLargeImpl( Scaffold( topBar = { + val selectedChapterCount = remember(chapters) { + chapters.count { it.selected } + } MangaToolbar( modifier = Modifier.onSizeChanged { topBarHeight = it.height }, title = state.manga.title, - titleAlphaProvider = { if (chapters.fastAny { it.selected }) 1f else 0f }, + titleAlphaProvider = { if (isAnySelected) 1f else 0f }, backgroundAlphaProvider = { 1f }, hasFilters = state.manga.chaptersFiltered(), onBackClicked = internalOnBackPressed, @@ -534,7 +565,7 @@ fun MangaScreenLargeImpl( onClickEditCategory = onEditCategoryClicked, onClickRefresh = onRefresh, onClickMigrate = onMigrateClicked, - actionModeCounter = chapters.count { it.selected }, + actionModeCounter = selectedChapterCount, onSelectAll = { onAllChapterSelected(true) }, onInvertSelection = { onInvertSelection() }, ) @@ -544,8 +575,11 @@ fun MangaScreenLargeImpl( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomEnd, ) { + val selectedChapters = remember(chapters) { + chapters.filter { it.selected } + } SharedMangaBottomActionMenu( - selected = chapters.filter { it.selected }, + selected = selectedChapters, onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, @@ -557,19 +591,20 @@ fun MangaScreenLargeImpl( }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { + val isFABVisible = remember(chapters) { + chapters.fastAny { !it.chapter.read } && !isAnySelected + } AnimatedVisibility( - visible = chapters.fastAny { !it.chapter.read } && chapters.fastAll { !it.selected }, + visible = isFABVisible, enter = fadeIn(), exit = fadeOut(), ) { ExtendedFloatingActionButton( text = { - val id = if (state.chapters.fastAny { it.chapter.read }) { - R.string.action_resume - } else { - R.string.action_start + val isReading = remember(state.chapters) { + state.chapters.fastAny { it.chapter.read } } - Text(text = stringResource(id)) + Text(text = stringResource(if (isReading) R.string.action_resume else R.string.action_start)) }, icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, onClick = onContinueReading, @@ -640,10 +675,13 @@ fun MangaScreenLargeImpl( key = MangaScreenItem.CHAPTER_HEADER, contentType = MangaScreenItem.CHAPTER_HEADER, ) { + val missingChapterCount = remember(chapters) { + chapters.map { it.chapter.chapterNumber }.missingChaptersCount() + } ChapterHeader( - enabled = chapters.fastAll { !it.selected }, + enabled = !isAnySelected, chapterCount = chapters.size, - missingChapterCount = chapters.map { it.chapter.chapterNumber }.missingChaptersCount(), + missingChapterCount = missingChapterCount, onClick = onFilterButtonClicked, ) } @@ -651,6 +689,7 @@ fun MangaScreenLargeImpl( sharedChapterItems( manga = state.manga, chapters = chapters, + dateRelativeTime = dateRelativeTime, dateFormat = dateFormat, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, @@ -712,6 +751,7 @@ private fun SharedMangaBottomActionMenu( private fun LazyListScope.sharedChapterItems( manga: Manga, chapters: List, + dateRelativeTime: Boolean, dateFormat: DateFormat, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, @@ -740,7 +780,11 @@ private fun LazyListScope.sharedChapterItems( date = chapterItem.chapter.dateUpload .takeIf { it > 0L } ?.let { - Date(it).toRelativeString(context, dateFormat) + Date(it).toRelativeString( + context, + dateRelativeTime, + dateFormat, + ) }, readProgress = chapterItem.chapter.lastPageRead .takeIf { !chapterItem.chapter.read && it > 0L } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt index eac2228f1..fa98e176b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt @@ -143,7 +143,7 @@ fun MangaBottomActionMenu( if (onMarkPreviousAsReadClicked != null) { Button( title = stringResource(R.string.action_mark_previous_as_read), - icon = ImageVector.vectorResource(id = R.drawable.ic_done_prev_24dp), + icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp), toConfirm = confirm[4], onLongClick = { onLongClickItem(4) }, onClick = onMarkPreviousAsReadClicked, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt index 84d969247..94f34eec2 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R -import tachiyomi.domain.manga.interactor.MAX_FETCH_INTERVAL +import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.presentation.core.components.WheelTextPicker @Composable @@ -67,7 +67,7 @@ fun SetIntervalDialog( contentAlignment = Alignment.Center, ) { val size = DpSize(width = maxWidth / 2, height = 128.dp) - val items = (0..MAX_FETCH_INTERVAL).map { + val items = (0..FetchInterval.MAX_INTERVAL).map { if (it == 0) { stringResource(R.string.label_default) } else { @@ -91,7 +91,7 @@ fun SetIntervalDialog( TextButton(onClick = { onValueChanged(selectedInterval) onDismissRequest() - },) { + }) { Text(text = stringResource(R.string.action_ok)) } }, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 5e0efe42b..4d9e320fe 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -286,7 +286,7 @@ fun ExpandableMangaDescription( ) { tags.forEach { TagsChip( - modifier = Modifier.padding(vertical = 4.dp), + modifier = DefaultTagChipModifier, text = it, onClick = { tagSelected = it @@ -302,7 +302,7 @@ fun ExpandableMangaDescription( ) { items(items = tags) { TagsChip( - modifier = Modifier.padding(vertical = 4.dp), + modifier = DefaultTagChipModifier, text = it, onClick = { tagSelected = it @@ -654,6 +654,8 @@ private fun MangaSummary( } } +private val DefaultTagChipModifier = Modifier.padding(vertical = 4.dp) + @Composable private fun TagsChip( text: String, diff --git a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt index 702d42396..c12ac59e8 100644 --- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt @@ -62,7 +62,7 @@ fun MoreScreen( WarningBanner( textRes = R.string.fdroid_warning, modifier = Modifier.clickable { - uriHandler.openUri("https://tachiyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version") + uriHandler.openUri("https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds") }, ) } @@ -108,11 +108,11 @@ fun MoreScreen( stringResource(R.string.paused) } else { "${stringResource(R.string.paused)} • ${ - pluralStringResource( - id = R.plurals.download_queue_summary, - count = pending, - pending, - ) + pluralStringResource( + id = R.plurals.download_queue_summary, + count = pending, + pending, + ) }" } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt similarity index 96% rename from app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt index 5e8973626..c852e03a3 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceModel.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt @@ -4,9 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import eu.kanade.presentation.more.settings.Preference.PreferenceItem import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.Tracker import tachiyomi.core.preference.Preference as PreferenceData sealed class Preference { @@ -133,10 +132,10 @@ sealed class Preference { ) : PreferenceItem() /** - * A [PreferenceItem] for individual tracking service. + * A [PreferenceItem] for individual tracker. */ - data class TrackingPreference( - val service: TrackService, + data class TrackerPreference( + val tracker: Tracker, override val title: String, val login: () -> Unit, val logout: () -> Unit, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt index 940a48225..b68f17fcd 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt @@ -156,13 +156,13 @@ internal fun PreferenceItem( }, ) } - is Preference.PreferenceItem.TrackingPreference -> { + is Preference.PreferenceItem.TrackerPreference -> { val uName by Injekt.get() - .getString(TrackPreferences.trackUsername(item.service.id)) + .getString(TrackPreferences.trackUsername(item.tracker.id)) .collectAsState() - item.service.run { + item.tracker.run { TrackingPreferenceWidget( - service = this, + tracker = this, checked = uName.isNotEmpty(), onClick = { if (isLoggedIn) item.logout() else item.login() }, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index cd600969c..cad067a98 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -34,7 +34,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.PREF_DOH_360 @@ -328,7 +328,7 @@ object SettingsAdvancedScreen : SearchableSettings { private fun getLibraryGroup(): Preference.PreferenceGroup { val scope = rememberCoroutineScope() val context = LocalContext.current - val trackManager = remember { Injekt.get() } + val trackerManager = remember { Injekt.get() } return Preference.PreferenceGroup( title = stringResource(R.string.label_library), @@ -340,7 +340,7 @@ object SettingsAdvancedScreen : SearchableSettings { Preference.PreferenceItem.TextPreference( title = stringResource(R.string.pref_refresh_library_tracking), subtitle = stringResource(R.string.pref_refresh_library_tracking_summary), - enabled = trackManager.hasLoggedServices(), + enabled = trackerManager.hasLoggedIn(), onClick = { LibraryUpdateJob.startNow(context, target = LibraryUpdateJob.Target.TRACKING) }, ), Preference.PreferenceItem.TextPreference( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt index d98198155..4540aee95 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt @@ -123,6 +123,11 @@ object SettingsAppearanceScreen : SearchableSettings { var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") } val now = remember { Date().time } + val dateFormat by uiPreferences.dateFormat().collectAsState() + val formattedNow = remember(dateFormat) { + UiPreferences.dateFormat(dateFormat).format(now) + } + LaunchedEffect(currentLanguage) { val locale = if (currentLanguage.isEmpty()) { LocaleListCompat.getEmptyLocaleList() @@ -162,6 +167,15 @@ object SettingsAppearanceScreen : SearchableSettings { "${it.ifEmpty { stringResource(R.string.label_default) }} ($formattedDate)" }, ), + Preference.PreferenceItem.SwitchPreference( + pref = uiPreferences.relativeTime(), + title = stringResource(R.string.pref_relative_format), + subtitle = stringResource( + R.string.pref_relative_format_summary, + stringResource(R.string.relative_time_today), + formattedNow, + ), + ), ), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupAndSyncScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupAndSyncScreen.kt index f4ad14532..12fdb8c8d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupAndSyncScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupAndSyncScreen.kt @@ -211,7 +211,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings { showCreateDialog = false flag = it try { - chooseBackupDir.launch(Backup.getBackupFilename()) + chooseBackupDir.launch(Backup.getFilename()) } catch (e: ActivityNotFoundException) { flag = 0 context.toast(R.string.file_picker_error) @@ -250,6 +250,8 @@ object SettingsBackupAndSyncScreen : SearchableSettings { BackupConst.BACKUP_CHAPTER to R.string.chapters, BackupConst.BACKUP_TRACK to R.string.track, BackupConst.BACKUP_HISTORY to R.string.history, + BackupConst.BACKUP_APP_PREFS to R.string.app_settings, + BackupConst.BACKUP_SOURCE_PREFS to R.string.source_settings, ) } val flags = remember { choices.keys.toMutableStateList() } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index 35552e225..0e71d4cdb 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -23,7 +23,7 @@ import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.widget.TriStateListDialog import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.ui.category.CategoryScreen import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -199,7 +199,7 @@ object SettingsLibraryScreen : SearchableSettings { ), Preference.PreferenceItem.SwitchPreference( pref = libraryPreferences.autoUpdateTrackers(), - enabled = Injekt.get().hasLoggedServices(), + enabled = Injekt.get().hasLoggedIn(), title = stringResource(R.string.pref_library_update_refresh_trackers), subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary), ), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt index bb73b89ec..e4ac76681 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt @@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType -import eu.kanade.tachiyomi.util.system.isReleaseBuildType import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -305,12 +304,6 @@ object SettingsReaderScreen : SearchableSettings { subtitle = stringResource(R.string.pref_dual_page_invert_summary), enabled = dualPageSplit, ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.longStripSplitWebtoon(), - title = stringResource(R.string.pref_long_strip_split), - subtitle = stringResource(R.string.split_tall_images_summary), - enabled = !isReleaseBuildType, // TODO: Show in release build when the feature is stable - ), Preference.PreferenceItem.SwitchPreference( pref = readerPreferences.webtoonDoubleTapZoomEnabled(), title = stringResource(R.string.pref_double_tap_zoom), @@ -349,11 +342,6 @@ object SettingsReaderScreen : SearchableSettings { pref = readerPreferences.readWithLongTap(), title = stringResource(R.string.pref_read_with_long_tap), ), - Preference.PreferenceItem.SwitchPreference( - pref = readerPreferences.folderPerManga(), - title = stringResource(R.string.pref_create_folder_per_manga), - subtitle = stringResource(R.string.pref_create_folder_per_manga_summary), - ), ), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index 3b4df1e0b..655d79cc2 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -44,9 +44,9 @@ import androidx.compose.ui.unit.dp import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.presentation.more.settings.Preference import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.Tracker +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.anilist.AnilistApi import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi @@ -70,7 +70,7 @@ object SettingsTrackingScreen : SearchableSettings { @Composable override fun RowScope.AppBarAction() { val uriHandler = LocalUriHandler.current - IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/help/guides/tracking/") }) { + IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) { Icon( imageVector = Icons.Outlined.HelpOutline, contentDescription = stringResource(R.string.tracking_guide), @@ -82,7 +82,7 @@ object SettingsTrackingScreen : SearchableSettings { override fun getPreferences(): List { val context = LocalContext.current val trackPreferences = remember { Injekt.get() } - val trackManager = remember { Injekt.get() } + val trackerManager = remember { Injekt.get() } val sourceManager = remember { Injekt.get() } var dialog by remember { mutableStateOf(null) } @@ -90,24 +90,24 @@ object SettingsTrackingScreen : SearchableSettings { when (this) { is LoginDialog -> { TrackingLoginDialog( - service = service, + tracker = tracker, uNameStringRes = uNameStringRes, onDismissRequest = { dialog = null }, ) } is LogoutDialog -> { TrackingLogoutDialog( - service = service, + tracker = tracker, onDismissRequest = { dialog = null }, ) } } } - val enhancedTrackers = trackManager.services - .filter { it is EnhancedTrackService } + val enhancedTrackers = trackerManager.trackers + .filter { it is EnhancedTracker } .partition { service -> - val acceptedSources = (service as EnhancedTrackService).getAcceptedSources() + val acceptedSources = (service as EnhancedTracker).getAcceptedSources() sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources } } var enhancedTrackerInfo = stringResource(R.string.enhanced_tracking_info) @@ -127,41 +127,41 @@ object SettingsTrackingScreen : SearchableSettings { Preference.PreferenceGroup( title = stringResource(R.string.services), preferenceItems = listOf( - Preference.PreferenceItem.TrackingPreference( - title = trackManager.myAnimeList.name, - service = trackManager.myAnimeList, + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.myAnimeList.name, + tracker = trackerManager.myAnimeList, login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) }, - logout = { dialog = LogoutDialog(trackManager.myAnimeList) }, + logout = { dialog = LogoutDialog(trackerManager.myAnimeList) }, ), - Preference.PreferenceItem.TrackingPreference( - title = trackManager.aniList.name, - service = trackManager.aniList, + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.aniList.name, + tracker = trackerManager.aniList, login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) }, - logout = { dialog = LogoutDialog(trackManager.aniList) }, + logout = { dialog = LogoutDialog(trackerManager.aniList) }, ), - Preference.PreferenceItem.TrackingPreference( - title = trackManager.kitsu.name, - service = trackManager.kitsu, - login = { dialog = LoginDialog(trackManager.kitsu, R.string.email) }, - logout = { dialog = LogoutDialog(trackManager.kitsu) }, + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.kitsu.name, + tracker = trackerManager.kitsu, + login = { dialog = LoginDialog(trackerManager.kitsu, R.string.email) }, + logout = { dialog = LogoutDialog(trackerManager.kitsu) }, ), - Preference.PreferenceItem.TrackingPreference( - title = trackManager.mangaUpdates.name, - service = trackManager.mangaUpdates, - login = { dialog = LoginDialog(trackManager.mangaUpdates, R.string.username) }, - logout = { dialog = LogoutDialog(trackManager.mangaUpdates) }, + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.mangaUpdates.name, + tracker = trackerManager.mangaUpdates, + login = { dialog = LoginDialog(trackerManager.mangaUpdates, R.string.username) }, + logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) }, ), - Preference.PreferenceItem.TrackingPreference( - title = trackManager.shikimori.name, - service = trackManager.shikimori, + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.shikimori.name, + tracker = trackerManager.shikimori, login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) }, - logout = { dialog = LogoutDialog(trackManager.shikimori) }, + logout = { dialog = LogoutDialog(trackerManager.shikimori) }, ), - Preference.PreferenceItem.TrackingPreference( - title = trackManager.bangumi.name, - service = trackManager.bangumi, + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.bangumi.name, + tracker = trackerManager.bangumi, login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, - logout = { dialog = LogoutDialog(trackManager.bangumi) }, + logout = { dialog = LogoutDialog(trackerManager.bangumi) }, ), Preference.PreferenceItem.InfoPreference(stringResource(R.string.tracking_info)), ), @@ -170,10 +170,10 @@ object SettingsTrackingScreen : SearchableSettings { title = stringResource(R.string.enhanced_services), preferenceItems = enhancedTrackers.first .map { service -> - Preference.PreferenceItem.TrackingPreference( + Preference.PreferenceItem.TrackerPreference( title = service.name, - service = service, - login = { (service as EnhancedTrackService).loginNoop() }, + tracker = service, + login = { (service as EnhancedTracker).loginNoop() }, logout = service::logout, ) } + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)), @@ -183,15 +183,15 @@ object SettingsTrackingScreen : SearchableSettings { @Composable private fun TrackingLoginDialog( - service: TrackService, + tracker: Tracker, @StringRes uNameStringRes: Int, onDismissRequest: () -> Unit, ) { val context = LocalContext.current val scope = rememberCoroutineScope() - var username by remember { mutableStateOf(TextFieldValue(service.getUsername())) } - var password by remember { mutableStateOf(TextFieldValue(service.getPassword())) } + var username by remember { mutableStateOf(TextFieldValue(tracker.getUsername())) } + var password by remember { mutableStateOf(TextFieldValue(tracker.getPassword())) } var processing by remember { mutableStateOf(false) } var inputError by remember { mutableStateOf(false) } @@ -200,7 +200,7 @@ object SettingsTrackingScreen : SearchableSettings { title = { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = stringResource(R.string.login_title, service.name), + text = stringResource(R.string.login_title, tracker.name), modifier = Modifier.weight(1f), ) IconButton(onClick = onDismissRequest) { @@ -264,7 +264,7 @@ object SettingsTrackingScreen : SearchableSettings { processing = true val result = checkLogin( context = context, - service = service, + tracker = tracker, username = username.text, password = password.text, ) @@ -283,16 +283,16 @@ object SettingsTrackingScreen : SearchableSettings { private suspend fun checkLogin( context: Context, - service: TrackService, + tracker: Tracker, username: String, password: String, ): Boolean { return try { - service.login(username, password) + tracker.login(username, password) withUIContext { context.toast(R.string.login_success) } true } catch (e: Throwable) { - service.logout() + tracker.logout() withUIContext { context.toast(e.message.toString()) } false } @@ -300,7 +300,7 @@ object SettingsTrackingScreen : SearchableSettings { @Composable private fun TrackingLogoutDialog( - service: TrackService, + tracker: Tracker, onDismissRequest: () -> Unit, ) { val context = LocalContext.current @@ -308,7 +308,7 @@ object SettingsTrackingScreen : SearchableSettings { onDismissRequest = onDismissRequest, title = { Text( - text = stringResource(R.string.logout_title, service.name), + text = stringResource(R.string.logout_title, tracker.name), textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), ) @@ -324,7 +324,7 @@ object SettingsTrackingScreen : SearchableSettings { Button( modifier = Modifier.weight(1f), onClick = { - service.logout() + tracker.logout() onDismissRequest() context.toast(R.string.logout_success) }, @@ -342,10 +342,10 @@ object SettingsTrackingScreen : SearchableSettings { } private data class LoginDialog( - val service: TrackService, + val tracker: Tracker, @StringRes val uNameStringRes: Int, ) private data class LogoutDialog( - val service: TrackService, + val tracker: Tracker, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt index 6f42edb7a..c12f3128e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt @@ -23,12 +23,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import compose.icons.SimpleIcons -import compose.icons.simpleicons.Discord -import compose.icons.simpleicons.Facebook -import compose.icons.simpleicons.Github -import compose.icons.simpleicons.Reddit -import compose.icons.simpleicons.Twitter import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.more.LogoHeader @@ -53,6 +47,12 @@ import tachiyomi.domain.release.interactor.GetApplicationRelease import tachiyomi.presentation.core.components.LinkIcon import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.icons.CustomIcons +import tachiyomi.presentation.core.icons.Discord +import tachiyomi.presentation.core.icons.Facebook +import tachiyomi.presentation.core.icons.Github +import tachiyomi.presentation.core.icons.Reddit +import tachiyomi.presentation.core.icons.X import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.text.DateFormat @@ -149,7 +149,7 @@ object AboutScreen : Screen() { item { TextPreferenceWidget( title = stringResource(R.string.help_translate), - onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/help/contribution/#translation") }, + onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/docs/contribute#translation") }, ) } @@ -163,7 +163,7 @@ object AboutScreen : Screen() { item { TextPreferenceWidget( title = stringResource(R.string.privacy_policy), - onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy") }, + onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy/") }, ) } @@ -181,27 +181,27 @@ object AboutScreen : Screen() { ) LinkIcon( label = "Discord", - icon = SimpleIcons.Discord, + icon = CustomIcons.Discord, url = "https://discord.gg/tachiyomi", ) LinkIcon( - label = "Twitter", - icon = SimpleIcons.Twitter, - url = "https://twitter.com/tachiyomiorg", + label = "X", + icon = CustomIcons.X, + url = "https://x.com/tachiyomiorg", ) LinkIcon( label = "Facebook", - icon = SimpleIcons.Facebook, + icon = CustomIcons.Facebook, url = "https://facebook.com/tachiyomiorg", ) LinkIcon( label = "Reddit", - icon = SimpleIcons.Reddit, + icon = CustomIcons.Reddit, url = "https://www.reddit.com/r/Tachiyomi", ) LinkIcon( label = "GitHub", - icon = SimpleIcons.Github, + icon = CustomIcons.Github, url = "https://github.com/tachiyomiorg", ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt index e5b94adb8..f1a8ae47c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/OpenSourceLicensesScreen.kt @@ -41,9 +41,9 @@ class OpenSourceLicensesScreen : Screen() { ), onLibraryClick = { val libraryLicenseScreen = OpenSourceLibraryLicenseScreen( - name = it.name, - website = it.website, - license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(), + name = it.library.name, + website = it.library.website, + license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(), ) navigator.push(libraryLicenseScreen) }, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt index e0c34fbc4..d8544d156 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt @@ -20,12 +20,12 @@ import androidx.compose.ui.unit.dp import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted import eu.kanade.presentation.track.components.TrackLogoIcon import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.Tracker @Composable fun TrackingPreferenceWidget( modifier: Modifier = Modifier, - service: TrackService, + tracker: Tracker, checked: Boolean, onClick: (() -> Unit)? = null, ) { @@ -38,9 +38,9 @@ fun TrackingPreferenceWidget( .padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { - TrackLogoIcon(service) + TrackLogoIcon(tracker) Text( - text = service.name, + text = tracker.name, modifier = Modifier .weight(1f) .padding(horizontal = 16.dp), diff --git a/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt index 9e277b757..0fbe079a9 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/OrientationModeSelectDialog.kt @@ -1,25 +1,25 @@ package eu.kanade.presentation.reader -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.FilterChip -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import eu.kanade.domain.manga.model.orientationType import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel -import tachiyomi.presentation.core.components.SettingsChipRow -import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.components.SettingsIconGrid +import tachiyomi.presentation.core.components.material.IconToggleButton private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it } @@ -32,22 +32,20 @@ fun OrientationModeSelectDialog( val manga by screenModel.mangaFlow.collectAsState() val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) } - AdaptiveSheet( - onDismissRequest = onDismissRequest, - ) { - Row( - modifier = Modifier.padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - ) { - SettingsChipRow(R.string.rotation_type) { - orientationTypeOptions.map { (stringRes, it) -> - FilterChip( - selected = it == orientationType, - onClick = { - screenModel.onChangeOrientation(it) + AdaptiveSheet(onDismissRequest = onDismissRequest) { + Box(modifier = Modifier.padding(vertical = 16.dp)) { + SettingsIconGrid(R.string.rotation_type) { + items(orientationTypeOptions) { (stringRes, mode) -> + IconToggleButton( + checked = mode == orientationType, + onCheckedChange = { + screenModel.onChangeOrientation(mode) onChange(stringRes) + onDismissRequest() }, - label = { Text(stringResource(stringRes)) }, + modifier = Modifier.fillMaxWidth(), + imageVector = ImageVector.vectorResource(mode.iconRes), + title = stringResource(stringRes), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt index c97515fb1..69df2a727 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt @@ -6,12 +6,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -@OptIn(ExperimentalTextApi::class) @Composable fun PageIndicatorText( currentPage: Int, diff --git a/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt b/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt index 91920d2ec..cb11d9950 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ReadingModeSelectDialog.kt @@ -1,24 +1,25 @@ package eu.kanade.presentation.reader -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.FilterChip +import androidx.compose.foundation.lazy.grid.items import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.vectorResource import eu.kanade.domain.manga.model.readingModeType import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType -import tachiyomi.presentation.core.components.SettingsChipRow +import tachiyomi.presentation.core.components.SettingsIconGrid +import tachiyomi.presentation.core.components.material.IconToggleButton import tachiyomi.presentation.core.components.material.padding private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it } @@ -32,22 +33,20 @@ fun ReadingModeSelectDialog( val manga by screenModel.mangaFlow.collectAsState() val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) } - AdaptiveSheet( - onDismissRequest = onDismissRequest, - ) { - Row( - modifier = Modifier.padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - ) { - SettingsChipRow(R.string.pref_category_reading_mode) { - readingModeOptions.map { (stringRes, it) -> - FilterChip( - selected = it == readingMode, - onClick = { - screenModel.onChangeReadingMode(it) + AdaptiveSheet(onDismissRequest = onDismissRequest) { + Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) { + SettingsIconGrid(R.string.pref_category_reading_mode) { + items(readingModeOptions) { (stringRes, mode) -> + IconToggleButton( + checked = mode == readingMode, + onCheckedChange = { + screenModel.onChangeReadingMode(mode) onChange(stringRes) + onDismissRequest() }, - label = { Text(stringResource(stringRes)) }, + modifier = Modifier.fillMaxWidth(), + imageVector = ImageVector.vectorResource(mode.iconRes), + title = stringResource(stringRes), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt index 874a053cd..07d6cb49a 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/ReadingModePage.kt @@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer -import eu.kanade.tachiyomi.util.system.isReleaseBuildType import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.SettingsChipRow @@ -185,13 +184,6 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM ) } - if (!isReleaseBuildType) { - CheckboxItem( - label = stringResource(R.string.pref_long_strip_split), - pref = screenModel.preferences.longStripSplitWebtoon(), - ) - } - CheckboxItem( label = stringResource(R.string.pref_double_tap_zoom), pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(), diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt index d9a899d5d..bf7860147 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt @@ -49,7 +49,7 @@ import eu.kanade.domain.track.model.toDbTrack import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.track.components.TrackLogoIcon import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.util.system.copyToClipboard import java.text.DateFormat @@ -80,12 +80,12 @@ fun TrackInfoDialogHome( ) { trackItems.forEach { item -> if (item.track != null) { - val supportsScoring = item.service.getScoreList().isNotEmpty() - val supportsReadingDates = item.service.supportsReadingDates + val supportsScoring = item.tracker.getScoreList().isNotEmpty() + val supportsReadingDates = item.tracker.supportsReadingDates TrackInfoItem( title = item.track.title, - service = item.service, - status = item.service.getStatus(item.track.status.toInt()), + tracker = item.tracker, + status = item.tracker.getStatus(item.track.status.toInt()), onStatusClick = { onStatusClick(item) }, chapters = "${item.track.lastChapterRead.toInt()}".let { val totalChapters = item.track.totalChapters @@ -97,7 +97,7 @@ fun TrackInfoDialogHome( } }, onChaptersClick = { onChapterClick(item) }, - score = item.service.displayScore(item.track.toDbTrack()) + score = item.tracker.displayScore(item.track.toDbTrack()) .takeIf { supportsScoring && item.track.score != 0.0 }, onScoreClick = { onScoreClick(item) } .takeIf { supportsScoring }, @@ -115,7 +115,7 @@ fun TrackInfoDialogHome( ) } else { TrackInfoItemEmpty( - service = item.service, + tracker = item.tracker, onNewSearch = { onNewSearch(item) }, ) } @@ -126,7 +126,7 @@ fun TrackInfoDialogHome( @Composable private fun TrackInfoItem( title: String, - service: TrackService, + tracker: Tracker, @StringRes status: Int?, onStatusClick: () -> Unit, chapters: String, @@ -147,7 +147,7 @@ private fun TrackInfoItem( verticalAlignment = Alignment.CenterVertically, ) { TrackLogoIcon( - service = service, + tracker = tracker, onClick = onOpenInBrowser, ) Box( @@ -260,13 +260,13 @@ private fun TrackDetailsItem( @Composable private fun TrackInfoItemEmpty( - service: TrackService, + tracker: Tracker, onNewSearch: () -> Unit, ) { Row( verticalAlignment = Alignment.CenterVertically, ) { - TrackLogoIcon(service) + TrackLogoIcon(tracker) TextButton( onClick = onNewSearch, modifier = Modifier diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackServiceSearch.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt similarity index 99% rename from app/src/main/java/eu/kanade/presentation/track/TrackServiceSearch.kt rename to app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt index 201f87607..c2b951453 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackServiceSearch.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearch.kt @@ -70,7 +70,7 @@ import tachiyomi.presentation.core.util.runOnEnterKeyPressed import tachiyomi.presentation.core.util.secondaryItemAlpha @Composable -fun TrackServiceSearch( +fun TrackerSearch( query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit, onDispatchQuery: () -> Unit, @@ -223,6 +223,7 @@ private fun SearchResultItem( val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent Box( modifier = Modifier + .fillMaxWidth() .padding(horizontal = 12.dp) .clip(shape) .background(MaterialTheme.colorScheme.surface) diff --git a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt index 44d98cbd2..52bf66575 100644 --- a/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt +++ b/app/src/main/java/eu/kanade/presentation/track/components/TrackLogoIcon.kt @@ -12,12 +12,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.Tracker import tachiyomi.presentation.core.util.clickableNoIndication @Composable fun TrackLogoIcon( - service: TrackService, + tracker: Tracker, onClick: (() -> Unit)? = null, ) { val modifier = if (onClick != null) { @@ -29,13 +29,13 @@ fun TrackLogoIcon( Box( modifier = modifier .size(48.dp) - .background(color = Color(service.getLogoColor()), shape = MaterialTheme.shapes.medium) + .background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium) .padding(4.dp), contentAlignment = Alignment.Center, ) { Image( - painter = painterResource(service.getLogo()), - contentDescription = service.name, + painter = painterResource(tracker.getLogo()), + contentDescription = tracker.name, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt index 45e28127e..b5210916e 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt @@ -21,7 +21,7 @@ fun UpdatesDeleteConfirmationDialog( TextButton(onClick = { onConfirm() onDismissRequest() - },) { + }) { Text(text = stringResource(R.string.action_ok)) } }, diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index 1f4a56d5f..1572faff4 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -43,6 +43,7 @@ fun UpdateScreen( state: UpdatesScreenModel.State, snackbarHostState: SnackbarHostState, lastUpdated: Long, + relativeTime: Boolean, onClickCover: (UpdatesItem) -> Unit, onSelectAll: (Boolean) -> Unit, onInvertSelection: () -> Unit, @@ -113,7 +114,7 @@ fun UpdateScreen( } updatesUiItems( - uiModels = state.getUiModel(context), + uiModels = state.getUiModel(context, relativeTime), selectionMode = state.selectionMode, onUpdateSelected = onUpdateSelected, onClickCover = onClickCover, diff --git a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt index 3a97d57ec..5f88a4fe3 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt @@ -175,7 +175,7 @@ fun WebViewScreenContent( WarningBanner( textRes = R.string.information_cloudflare_help, modifier = Modifier.clickable { - uriHandler.openUri("https://tachiyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues") + uriHandler.openUri("https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare") }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index ab8cf126d..595f1d3b1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -19,8 +19,8 @@ import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.data.saver.ImageSaver +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService -import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.network.JavaScriptEngine import eu.kanade.tachiyomi.network.NetworkHelper @@ -134,7 +134,7 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { DownloadManager(app) } addSingletonFactory { DownloadCache(app) } - addSingletonFactory { TrackManager(app) } + addSingletonFactory { TrackerManager() } addSingletonFactory { DelayedTrackingStore(app) } addSingletonFactory { ImageSaver(app) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 75edfdd46..bbdcaa6ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -9,15 +9,15 @@ import eu.kanade.domain.ui.UiPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.system.DeviceUtil -import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.workManager +import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getAndSet @@ -47,7 +47,7 @@ object Migrations { libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, - trackManager: TrackManager, + trackerManager: TrackerManager, ): Boolean { val lastVersionCode = preferenceStore.getInt("last_version_code", 0) val oldVersion = lastVersionCode.get() @@ -135,8 +135,8 @@ object Migrations { // Force MAL log out due to login flow change // v52: switched from scraping to WebView // v53: switched from WebView to OAuth - if (trackManager.myAnimeList.isLoggedIn) { - trackManager.myAnimeList.logout() + if (trackerManager.myAnimeList.isLoggedIn) { + trackerManager.myAnimeList.logout() context.toast(R.string.myanimelist_relogin) } } @@ -342,7 +342,7 @@ object Migrations { "pref_filter_library_started", "pref_filter_library_bookmarked", "pref_filter_library_completed", - ) + trackManager.services.map { "pref_filter_library_tracked_${it.id}" } + ) + trackerManager.trackers.map { "pref_filter_library_tracked_${it.id}" } prefKeys.forEach { key -> val pref = preferenceStore.getInt(key, 0) @@ -362,19 +362,31 @@ object Migrations { if (oldVersion < 100) { BackupCreateJob.setupTask(context) } - if (oldVersion < 102) { - // This was accidentally visible from the reader settings sheet, but should always - // be disabled in release builds. - if (isReleaseBuildType) { - readerPreferences.longStripSplitWebtoon().set(false) - } - } if (oldVersion < 105) { val pref = libraryPreferences.autoUpdateDeviceRestrictions() if (pref.isSet() && "battery_not_low" in pref.get()) { pref.getAndSet { it - "battery_not_low" } } } + if (oldVersion < 106) { + val pref = preferenceStore.getInt("relative_time", 7) + if (pref.get() == 0) { + uiPreferences.relativeTime().set(false) + } + } + if (oldVersion < 107) { + preferenceStore.getAll() + .filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") } + .forEach { (key, value) -> + if (value is String) { + preferenceStore + .getString(Preference.privateKey(key)) + .set(value) + + preferenceStore.getString(key).delete() + } + } + } return true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt index add7b3813..6bc4771dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt @@ -4,11 +4,21 @@ package eu.kanade.tachiyomi.data.backup internal object BackupConst { const val BACKUP_CATEGORY = 0x1 const val BACKUP_CATEGORY_MASK = 0x1 + const val BACKUP_CHAPTER = 0x2 const val BACKUP_CHAPTER_MASK = 0x2 + const val BACKUP_HISTORY = 0x4 const val BACKUP_HISTORY_MASK = 0x4 + const val BACKUP_TRACK = 0x8 const val BACKUP_TRACK_MASK = 0x8 - const val BACKUP_ALL = 0xF + + const val BACKUP_APP_PREFS = 0x10 + const val BACKUP_APP_PREFS_MASK = 0x10 + + const val BACKUP_SOURCE_PREFS = 0x20 + const val BACKUP_SOURCE_PREFS_MASK = 0x20 + + const val BACKUP_ALL = 0x3F } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index 6f960edec..875039e86 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt @@ -49,7 +49,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete } return try { - val location = BackupManager(context).createBackup(uri, flags, isAutoBackup) + val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup) if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())) Result.success() } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt new file mode 100644 index 000000000..b70df331f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt @@ -0,0 +1,268 @@ +package eu.kanade.tachiyomi.data.backup + +import android.Manifest +import android.content.Context +import android.net.Uri +import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK +import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK +import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.data.backup.models.BackupCategory +import eu.kanade.tachiyomi.data.backup.models.BackupHistory +import eu.kanade.tachiyomi.data.backup.models.BackupManga +import eu.kanade.tachiyomi.data.backup.models.BackupPreference +import eu.kanade.tachiyomi.data.backup.models.BackupSerializer +import eu.kanade.tachiyomi.data.backup.models.BackupSource +import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper +import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper +import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.preferenceKey +import eu.kanade.tachiyomi.source.sourcePreferences +import eu.kanade.tachiyomi.util.system.hasPermission +import kotlinx.serialization.protobuf.ProtoBuf +import logcat.LogPriority +import okio.buffer +import okio.gzip +import okio.sink +import tachiyomi.core.preference.Preference +import tachiyomi.core.preference.PreferenceStore +import tachiyomi.core.util.system.logcat +import tachiyomi.data.DatabaseHandler +import tachiyomi.domain.backup.service.BackupPreferences +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.category.model.Category +import tachiyomi.domain.history.interactor.GetHistory +import tachiyomi.domain.manga.interactor.GetFavorites +import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.source.service.SourceManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.FileOutputStream + +class BackupCreator( + private val context: Context, +) { + + private val handler: DatabaseHandler = Injekt.get() + private val sourceManager: SourceManager = Injekt.get() + private val backupPreferences: BackupPreferences = Injekt.get() + private val getCategories: GetCategories = Injekt.get() + private val getFavorites: GetFavorites = Injekt.get() + private val getHistory: GetHistory = Injekt.get() + private val preferenceStore: PreferenceStore = Injekt.get() + + internal val parser = ProtoBuf + + /** + * Create backup file. + * + * @param uri path of Uri + * @param isAutoBackup backup called from scheduled backup job + */ + suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String { + if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + throw IllegalStateException(context.getString(R.string.missing_storage_permission)) + } + + val databaseManga = getFavorites.await() + val backup = Backup( + backupMangas(databaseManga, flags), + backupCategories(flags), + emptyList(), + prepExtensionInfoForSync(databaseManga), + backupAppPreferences(flags), + backupSourcePreferences(flags), + ) + + var file: UniFile? = null + try { + file = ( + if (isAutoBackup) { + // Get dir of file and create + var dir = UniFile.fromUri(context, uri) + dir = dir.createDirectory("automatic") + + // Delete older backups + val numberOfBackups = backupPreferences.numberOfBackups().get() + dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) } + .orEmpty() + .sortedByDescending { it.name } + .drop(numberOfBackups - 1) + .forEach { it.delete() } + + // Create new file to place backup + dir.createFile(Backup.getFilename()) + } else { + UniFile.fromUri(context, uri) + } + ) + ?: throw Exception(context.getString(R.string.create_backup_file_error)) + + if (!file.isFile) { + throw IllegalStateException("Failed to get handle on a backup file") + } + + val byteArray = parser.encodeToByteArray(BackupSerializer, backup) + if (byteArray.isEmpty()) { + throw IllegalStateException(context.getString(R.string.empty_backup_error)) + } + + file.openOutputStream().also { + // Force overwrite old file + (it as? FileOutputStream)?.channel?.truncate(0) + }.sink().gzip().buffer().use { it.write(byteArray) } + val fileUri = file.uri + + // Make sure it's a valid backup file + BackupFileValidator().validate(context, fileUri) + + return fileUri.toString() + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + file?.delete() + throw e + } + } + + private fun prepExtensionInfoForSync(mangas: List): List { + return mangas + .asSequence() + .map(Manga::source) + .distinct() + .map(sourceManager::getOrStub) + .map(BackupSource::copyFrom) + .toList() + } + + /** + * Backup the categories of library + * + * @return list of [BackupCategory] to be backed up + */ + private suspend fun backupCategories(options: Int): List { + // Check if user wants category information in backup + return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { + getCategories.await() + .filterNot(Category::isSystemCategory) + .map(backupCategoryMapper) + } else { + emptyList() + } + } + + private suspend fun backupMangas(mangas: List, flags: Int): List { + return mangas.map { + backupManga(it, flags) + } + } + + /** + * Convert a manga to Json + * + * @param manga manga that gets converted + * @param options options for the backup + * @return [BackupManga] containing manga in a serializable form + */ + private suspend fun backupManga(manga: Manga, options: Int): BackupManga { + // Entry for this manga + val mangaObject = BackupManga.copyFrom(manga) + + // Check if user wants chapter information in backup + if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { + // Backup all the chapters + val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) } + if (chapters.isNotEmpty()) { + mangaObject.chapters = chapters + } + } + + // Check if user wants category information in backup + if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { + // Backup categories for this manga + val categoriesForManga = getCategories.await(manga.id) + if (categoriesForManga.isNotEmpty()) { + mangaObject.categories = categoriesForManga.map { it.order } + } + } + + // Check if user wants track information in backup + if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { + val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) } + if (tracks.isNotEmpty()) { + mangaObject.tracking = tracks + } + } + + // Check if user wants history information in backup + if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { + val historyByMangaId = getHistory.await(manga.id) + if (historyByMangaId.isNotEmpty()) { + val history = historyByMangaId.map { history -> + val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) } + BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration) + } + if (history.isNotEmpty()) { + mangaObject.history = history + } + } + } + + return mangaObject + } + + private fun backupAppPreferences(flags: Int): List { + if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList() + + return preferenceStore.getAll().toBackupPreferences() + } + + private fun backupSourcePreferences(flags: Int): List { + if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList() + + return sourceManager.getOnlineSources() + .filterIsInstance() + .map { + BackupSourcePreferences( + it.preferenceKey(), + it.sourcePreferences().all.toBackupPreferences(), + ) + } + } + + @Suppress("UNCHECKED_CAST") + private fun Map.toBackupPreferences(): List { + return this.filterKeys { !Preference.isPrivate(it) } + .mapNotNull { (key, value) -> + when (value) { + is Int -> BackupPreference(key, IntPreferenceValue(value)) + is Long -> BackupPreference(key, LongPreferenceValue(value)) + is Float -> BackupPreference(key, FloatPreferenceValue(value)) + is String -> BackupPreference(key, StringPreferenceValue(value)) + is Boolean -> BackupPreference(key, BooleanPreferenceValue(value)) + is Set<*> -> (value as? Set)?.let { + BackupPreference(key, StringSetPreferenceValue(it)) + } + else -> null + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt index 0fe9ddff9..8450ab367 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.util.BackupUtil import tachiyomi.domain.source.service.SourceManager import uy.kohesive.injekt.Injekt @@ -11,7 +11,7 @@ import uy.kohesive.injekt.api.get class BackupFileValidator( private val sourceManager: SourceManager = Injekt.get(), - private val trackManager: TrackManager = Injekt.get(), + private val trackerManager: TrackerManager = Injekt.get(), ) { /** @@ -50,7 +50,7 @@ class BackupFileValidator( .map { it.syncId } .distinct() val missingTrackers = trackers - .mapNotNull { trackManager.getService(it.toLong()) } + .mapNotNull { trackerManager.get(it.toLong()) } .filter { !it.isLoggedIn } .map { it.name } .sorted() @@ -58,5 +58,8 @@ class BackupFileValidator( return Results(missingSources, missingTrackers) } - data class Results(val missingSources: List, val missingTrackers: List) + data class Results( + val missingSources: List, + val missingTrackers: List, + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index 817988c31..e69de29bb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -1,590 +0,0 @@ -package eu.kanade.tachiyomi.data.backup - -import android.Manifest -import android.content.Context -import android.net.Uri -import com.hippo.unifile.UniFile -import eu.kanade.domain.chapter.model.copyFrom -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK -import eu.kanade.tachiyomi.data.backup.models.Backup -import eu.kanade.tachiyomi.data.backup.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.models.BackupHistory -import eu.kanade.tachiyomi.data.backup.models.BackupManga -import eu.kanade.tachiyomi.data.backup.models.BackupSerializer -import eu.kanade.tachiyomi.data.backup.models.BackupSource -import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper -import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper -import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper -import eu.kanade.tachiyomi.source.model.copyFrom -import eu.kanade.tachiyomi.util.system.hasPermission -import kotlinx.serialization.protobuf.ProtoBuf -import logcat.LogPriority -import okio.buffer -import okio.gzip -import okio.sink -import tachiyomi.core.util.system.logcat -import tachiyomi.data.DatabaseHandler -import tachiyomi.data.Manga_sync -import tachiyomi.data.Mangas -import tachiyomi.data.UpdateStrategyColumnAdapter -import tachiyomi.domain.backup.service.BackupPreferences -import tachiyomi.domain.category.interactor.GetCategories -import tachiyomi.domain.category.model.Category -import tachiyomi.domain.history.interactor.GetHistory -import tachiyomi.domain.history.model.HistoryUpdate -import tachiyomi.domain.library.service.LibraryPreferences -import tachiyomi.domain.manga.interactor.GetFavorites -import tachiyomi.domain.manga.model.Manga -import tachiyomi.domain.source.service.SourceManager -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.FileOutputStream -import java.util.Date -import kotlin.math.max - -class BackupManager( - private val context: Context, -) { - - private val handler: DatabaseHandler = Injekt.get() - private val sourceManager: SourceManager = Injekt.get() - private val backupPreferences: BackupPreferences = Injekt.get() - private val libraryPreferences: LibraryPreferences = Injekt.get() - private val getCategories: GetCategories = Injekt.get() - private val getFavorites: GetFavorites = Injekt.get() - private val getHistory: GetHistory = Injekt.get() - - internal val parser = ProtoBuf - - /** - * Create backup file from database - * - * @param uri path of Uri - * @param isAutoBackup backup called from scheduled backup job - */ - suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String { - if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - throw IllegalStateException(context.getString(R.string.missing_storage_permission)) - } - - val databaseManga = getFavorites.await() - val backup = Backup( - backupMangas(databaseManga, flags), - backupCategories(flags), - emptyList(), - prepExtensionInfoForSync(databaseManga), - ) - - var file: UniFile? = null - try { - file = ( - if (isAutoBackup) { - // Get dir of file and create - var dir = UniFile.fromUri(context, uri) - dir = dir.createDirectory("automatic") - - // Delete older backups - val numberOfBackups = backupPreferences.numberOfBackups().get() - val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""") - dir.listFiles { _, filename -> backupRegex.matches(filename) } - .orEmpty() - .sortedByDescending { it.name } - .drop(numberOfBackups - 1) - .forEach { it.delete() } - - // Create new file to place backup - dir.createFile(Backup.getBackupFilename()) - } else { - UniFile.fromUri(context, uri) - } - ) - ?: throw Exception(context.getString(R.string.create_backup_file_error)) - - if (!file.isFile) { - throw IllegalStateException("Failed to get handle on a backup file") - } - - val byteArray = parser.encodeToByteArray(BackupSerializer, backup) - if (byteArray.isEmpty()) { - throw IllegalStateException(context.getString(R.string.empty_backup_error)) - } - - file.openOutputStream().also { - // Force overwrite old file - (it as? FileOutputStream)?.channel?.truncate(0) - }.sink().gzip().buffer().use { it.write(byteArray) } - val fileUri = file.uri - - // Make sure it's a valid backup file - BackupFileValidator().validate(context, fileUri) - - return fileUri.toString() - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - file?.delete() - throw e - } - } - - fun prepExtensionInfoForSync(mangas: List): List { - return mangas - .asSequence() - .map(Manga::source) - .distinct() - .map(sourceManager::getOrStub) - .map(BackupSource::copyFrom) - .toList() - } - - /** - * Backup the categories of library - * - * @return list of [BackupCategory] to be backed up - */ - suspend fun backupCategories(options: Int): List { - // Check if user wants category information in backup - return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { - getCategories.await() - .filterNot(Category::isSystemCategory) - .map(backupCategoryMapper) - } else { - emptyList() - } - } - - suspend fun backupMangas(mangas: List, flags: Int): List { - return mangas.map { - backupManga(it, flags) - } - } - - /** - * Convert a manga to Json - * - * @param manga manga that gets converted - * @param options options for the backup - * @return [BackupManga] containing manga in a serializable form - */ - private suspend fun backupManga(manga: Manga, options: Int): BackupManga { - // Entry for this manga - val mangaObject = BackupManga.copyFrom(manga) - - // Check if user wants chapter information in backup - if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { - // Backup all the chapters - val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) } - if (chapters.isNotEmpty()) { - mangaObject.chapters = chapters - } - } - - // Check if user wants category information in backup - if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { - // Backup categories for this manga - val categoriesForManga = getCategories.await(manga.id) - if (categoriesForManga.isNotEmpty()) { - mangaObject.categories = categoriesForManga.map { it.order } - } - } - - // Check if user wants track information in backup - if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { - val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) } - if (tracks.isNotEmpty()) { - mangaObject.tracking = tracks - } - } - - // Check if user wants history information in backup - if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { - val historyByMangaId = getHistory.await(manga.id) - if (historyByMangaId.isNotEmpty()) { - val history = historyByMangaId.map { history -> - val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) } - BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration) - } - if (history.isNotEmpty()) { - mangaObject.history = history - } - } - } - - return mangaObject - } - - internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga { - var updatedManga = manga.copy(id = dbManga._id) - updatedManga = updatedManga.copyFrom(dbManga) - updateManga(updatedManga) - return updatedManga - } - - /** - * Fetches manga information - * - * @param manga manga that needs updating - * @return Updated manga info. - */ - internal suspend fun restoreNewManga(manga: Manga): Manga { - return manga.copy( - initialized = manga.description != null, - id = insertManga(manga), - ) - } - - /** - * Restore the categories from Json - * - * @param backupCategories list containing categories - */ - internal suspend fun restoreCategories(backupCategories: List) { - // Get categories from file and from db - val dbCategories = getCategories.await() - - val categories = backupCategories.map { - var category = it.getCategory() - var found = false - for (dbCategory in dbCategories) { - // If the category is already in the db, assign the id to the file's category - // and do nothing - if (category.name == dbCategory.name) { - category = category.copy(id = dbCategory.id) - found = true - break - } - } - if (!found) { - // Let the db assign the id - val id = handler.awaitOneExecutable { - categoriesQueries.insert(category.name, category.order, category.flags) - categoriesQueries.selectLastInsertedRowId() - } - category = category.copy(id = id) - } - - category - } - - libraryPreferences.categorizedDisplaySettings().set( - (dbCategories + categories) - .distinctBy { it.flags } - .size > 1, - ) - } - - /** - * Restores the categories a manga is in. - * - * @param manga the manga whose categories have to be restored. - * @param categories the categories to restore. - */ - internal suspend fun restoreCategories(manga: Manga, categories: List, backupCategories: List) { - val dbCategories = getCategories.await() - val mangaCategoriesToUpdate = mutableListOf>() - - categories.forEach { backupCategoryOrder -> - backupCategories.firstOrNull { - it.order == backupCategoryOrder.toLong() - }?.let { backupCategory -> - dbCategories.firstOrNull { dbCategory -> - dbCategory.name == backupCategory.name - }?.let { dbCategory -> - mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id)) - } - } - } - - // Update database - if (mangaCategoriesToUpdate.isNotEmpty()) { - handler.await(true) { - mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id) - mangaCategoriesToUpdate.forEach { (mangaId, categoryId) -> - mangas_categoriesQueries.insert(mangaId, categoryId) - } - } - } - } - - /** - * Restore history from Json - * - * @param history list containing history to be restored - */ - internal suspend fun restoreHistory(history: List) { - // List containing history to be updated - val toUpdate = mutableListOf() - for ((url, lastRead, readDuration) in history) { - var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) } - // Check if history already in database and update - if (dbHistory != null) { - dbHistory = dbHistory.copy( - last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)), - time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read, - ) - toUpdate.add( - HistoryUpdate( - chapterId = dbHistory.chapter_id, - readAt = dbHistory.last_read!!, - sessionReadDuration = dbHistory.time_read, - ), - ) - } else { - // If not in database create - handler - .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) } - ?.let { - toUpdate.add( - HistoryUpdate( - chapterId = it._id, - readAt = Date(lastRead), - sessionReadDuration = readDuration, - ), - ) - } - } - } - handler.await(true) { - toUpdate.forEach { payload -> - historyQueries.upsert( - payload.chapterId, - payload.readAt, - payload.sessionReadDuration, - ) - } - } - } - - /** - * Restores the sync of a manga. - * - * @param manga the manga whose sync have to be restored. - * @param tracks the track list to restore. - */ - internal suspend fun restoreTracking(manga: Manga, tracks: List) { - // Get tracks from database - val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) } - val toUpdate = mutableListOf() - val toInsert = mutableListOf() - - tracks - // Fix foreign keys with the current manga id - .map { it.copy(mangaId = manga.id) } - .forEach { track -> - var isInDatabase = false - for (dbTrack in dbTracks) { - if (track.syncId == dbTrack.sync_id) { - // The sync is already in the db, only update its fields - var temp = dbTrack - if (track.remoteId != dbTrack.remote_id) { - temp = temp.copy(remote_id = track.remoteId) - } - if (track.libraryId != dbTrack.library_id) { - temp = temp.copy(library_id = track.libraryId) - } - temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead)) - isInDatabase = true - toUpdate.add(temp) - break - } - } - if (!isInDatabase) { - // Insert new sync. Let the db assign the id - toInsert.add(track.copy(id = 0)) - } - } - - // Update database - if (toUpdate.isNotEmpty()) { - handler.await(true) { - toUpdate.forEach { track -> - manga_syncQueries.update( - track.manga_id, - track.sync_id, - track.remote_id, - track.library_id, - track.title, - track.last_chapter_read, - track.total_chapters, - track.status, - track.score, - track.remote_url, - track.start_date, - track.finish_date, - track._id, - ) - } - } - } - if (toInsert.isNotEmpty()) { - handler.await(true) { - toInsert.forEach { track -> - manga_syncQueries.insert( - track.mangaId, - track.syncId, - track.remoteId, - track.libraryId, - track.title, - track.lastChapterRead, - track.totalChapters, - track.status, - track.score, - track.remoteUrl, - track.startDate, - track.finishDate, - ) - } - } - } - } - - internal suspend fun restoreChapters(manga: Manga, chapters: List) { - val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) } - - val processed = chapters.map { chapter -> - var updatedChapter = chapter - val dbChapter = dbChapters.find { it.url == updatedChapter.url } - if (dbChapter != null) { - updatedChapter = updatedChapter.copy(id = dbChapter._id) - updatedChapter = updatedChapter.copyFrom(dbChapter) - if (dbChapter.read != chapter.read) { - updatedChapter = updatedChapter.copy(read = chapter.read, lastPageRead = chapter.lastPageRead) - } else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) { - updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read) - } - if (!updatedChapter.bookmark && dbChapter.bookmark) { - updatedChapter = updatedChapter.copy(bookmark = true) - } - } - - updatedChapter.copy(mangaId = manga.id) - } - - val newChapters = processed.groupBy { it.id > 0 } - newChapters[true]?.let { updateKnownChapters(it) } - newChapters[false]?.let { insertChapters(it) } - } - - /** - * Returns manga - * - * @return [Manga], null if not found - */ - internal suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? { - return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) } - } - - /** - * Inserts manga and returns id - * - * @return id of [Manga], null if not found - */ - private suspend fun insertManga(manga: Manga): Long { - return handler.awaitOneExecutable(true) { - mangasQueries.insert( - source = manga.source, - url = manga.url, - artist = manga.artist, - author = manga.author, - description = manga.description, - genre = manga.genre, - title = manga.title, - status = manga.status, - thumbnailUrl = manga.thumbnailUrl, - favorite = manga.favorite, - lastUpdate = manga.lastUpdate, - nextUpdate = 0L, - calculateInterval = 0L, - initialized = manga.initialized, - viewerFlags = manga.viewerFlags, - chapterFlags = manga.chapterFlags, - coverLastModified = manga.coverLastModified, - dateAdded = manga.dateAdded, - updateStrategy = manga.updateStrategy, - ) - mangasQueries.selectLastInsertedRowId() - } - } - - suspend fun updateManga(manga: Manga): Long { - handler.await(true) { - mangasQueries.update( - source = manga.source, - url = manga.url, - artist = manga.artist, - author = manga.author, - description = manga.description, - genre = manga.genre?.joinToString(separator = ", "), - title = manga.title, - status = manga.status, - thumbnailUrl = manga.thumbnailUrl, - favorite = manga.favorite, - lastUpdate = manga.lastUpdate, - nextUpdate = null, - calculateInterval = null, - initialized = manga.initialized, - viewer = manga.viewerFlags, - chapterFlags = manga.chapterFlags, - coverLastModified = manga.coverLastModified, - dateAdded = manga.dateAdded, - mangaId = manga.id, - updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), - ) - } - return manga.id - } - - /** - * Inserts list of chapters - */ - private suspend fun insertChapters(chapters: List) { - handler.await(true) { - chapters.forEach { chapter -> - chaptersQueries.insert( - chapter.mangaId, - chapter.url, - chapter.name, - chapter.scanlator, - chapter.read, - chapter.bookmark, - chapter.lastPageRead, - chapter.chapterNumber, - chapter.sourceOrder, - chapter.dateFetch, - chapter.dateUpload, - ) - } - } - } - - /** - * Updates a list of chapters with known database ids - */ - private suspend fun updateKnownChapters(chapters: List) { - handler.await(true) { - chapters.forEach { chapter -> - chaptersQueries.update( - mangaId = null, - url = null, - name = null, - scanlator = null, - read = chapter.read, - bookmark = chapter.bookmark, - lastPageRead = chapter.lastPageRead, - chapterNumber = null, - sourceOrder = null, - dateFetch = null, - dateUpload = null, - chapterId = chapter.id, - ) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt index 4f1c45bc2..170ddb80f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt @@ -26,7 +26,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet override suspend fun doWork(): Result { val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: return Result.failure() - val sync = inputData.getBoolean(SYNC, false) + val sync = inputData.getBoolean(SYNC_KEY, false) try { setForeground(getForegroundInfo()) @@ -67,7 +67,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet fun start(context: Context, uri: Uri, sync: Boolean = false) { val inputData = workDataOf( LOCATION_URI_KEY to uri.toString(), - SYNC to sync, + SYNC_KEY to sync, ) val request = OneTimeWorkRequestBuilder() .addTag(TAG) @@ -85,5 +85,4 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet private const val TAG = "BackupRestore" private const val LOCATION_URI_KEY = "location_uri" // String - -private const val SYNC = "sync" // Boolean +private const val SYNC_KEY = "sync" // Boolean diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index 544c5c18d..c912be3f9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -2,19 +2,38 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri +import eu.kanade.domain.chapter.model.copyFrom import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupManga +import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupSource +import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue +import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue +import eu.kanade.tachiyomi.source.model.copyFrom +import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.util.BackupUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.isActive +import tachiyomi.core.preference.AndroidPreferenceStore +import tachiyomi.core.preference.PreferenceStore +import tachiyomi.data.DatabaseHandler +import tachiyomi.data.Manga_sync +import tachiyomi.data.Mangas +import tachiyomi.data.UpdateStrategyColumnAdapter +import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.chapter.repository.ChapterRepository -import tachiyomi.domain.manga.interactor.SetFetchInterval +import tachiyomi.domain.history.model.HistoryUpdate +import tachiyomi.domain.library.service.LibraryPreferences +import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.model.Track import uy.kohesive.injekt.Injekt @@ -24,19 +43,23 @@ import java.text.SimpleDateFormat import java.time.ZonedDateTime import java.util.Date import java.util.Locale +import kotlin.math.max class BackupRestorer( private val context: Context, private val notifier: BackupNotifier, ) { + + private val handler: DatabaseHandler = Injekt.get() private val updateManga: UpdateManga = Injekt.get() - private val chapterRepository: ChapterRepository = Injekt.get() - private val setFetchInterval: SetFetchInterval = Injekt.get() + private val getCategories: GetCategories = Injekt.get() + private val fetchInterval: FetchInterval = Injekt.get() + + private val preferenceStore: PreferenceStore = Injekt.get() + private val libraryPreferences: LibraryPreferences = Injekt.get() private var now = ZonedDateTime.now() - private var currentFetchWindow = setFetchInterval.getWindow(now) - - private var backupManager = BackupManager(context) + private var currentFetchWindow = fetchInterval.getWindow(now) private var restoreAmount = 0 private var restoreProgress = 0 @@ -92,7 +115,7 @@ class BackupRestorer( private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean { val backup = BackupUtil.decodeBackup(context, uri) - restoreAmount = backup.backupManga.size + 1 // +1 for categories + restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs // Restore categories if (backup.backupCategories.isNotEmpty()) { @@ -103,9 +126,12 @@ class BackupRestorer( val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources sourceMapping = backupMaps.associate { it.sourceId to it.name } now = ZonedDateTime.now() - currentFetchWindow = setFetchInterval.getWindow(now) + currentFetchWindow = fetchInterval.getWindow(now) return coroutineScope { + restoreAppPreferences(backup.backupPreferences) + restoreSourcePreferences(backup.backupSourcePreferences) + // Restore individual manga backup.backupManga.forEach { if (!isActive) { @@ -115,12 +141,44 @@ class BackupRestorer( restoreManga(it, backup.backupCategories, sync) } // TODO: optionally trigger online library + tracker update + true } } private suspend fun restoreCategories(backupCategories: List) { - backupManager.restoreCategories(backupCategories) + // Get categories from file and from db + val dbCategories = getCategories.await() + + val categories = backupCategories.map { + var category = it.getCategory() + var found = false + for (dbCategory in dbCategories) { + // If the category is already in the db, assign the id to the file's category + // and do nothing + if (category.name == dbCategory.name) { + category = category.copy(id = dbCategory.id) + found = true + break + } + } + if (!found) { + // Let the db assign the id + val id = handler.awaitOneExecutable { + categoriesQueries.insert(category.name, category.order, category.flags) + categoriesQueries.selectLastInsertedRowId() + } + category = category.copy(id = id) + } + + category + } + + libraryPreferences.categorizedDisplaySettings().set( + (dbCategories + categories) + .distinctBy { it.flags } + .size > 1, + ) restoreProgress += 1 showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup)) @@ -135,14 +193,14 @@ class BackupRestorer( val tracks = backupManga.getTrackingImpl() try { - val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source) + val dbManga = getMangaFromDatabase(manga.url, manga.source) val restoredManga = if (dbManga == null) { // Manga not in database restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories) } else { // Manga in database // Copy information from manga already in database - val updatedManga = backupManager.restoreExistingManga(manga, dbManga) + val updatedManga = restoreExistingManga(manga, dbManga) // Fetch rest of manga information restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories) } @@ -160,6 +218,50 @@ class BackupRestorer( } } + /** + * Returns manga + * + * @return [Manga], null if not found + */ + private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? { + return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) } + } + + private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga { + var updatedManga = manga.copy(id = dbManga._id) + updatedManga = updatedManga.copyFrom(dbManga) + updateManga(updatedManga) + return updatedManga + } + + private suspend fun updateManga(manga: Manga): Long { + handler.await(true) { + mangasQueries.update( + source = manga.source, + url = manga.url, + artist = manga.artist, + author = manga.author, + description = manga.description, + genre = manga.genre?.joinToString(separator = ", "), + title = manga.title, + status = manga.status, + thumbnailUrl = manga.thumbnailUrl, + favorite = manga.favorite, + lastUpdate = manga.lastUpdate, + nextUpdate = null, + calculateInterval = null, + initialized = manga.initialized, + viewer = manga.viewerFlags, + chapterFlags = manga.chapterFlags, + coverLastModified = manga.coverLastModified, + dateAdded = manga.dateAdded, + mangaId = manga.id, + updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), + ) + } + return manga.id + } + /** * Fetches manga information * @@ -175,12 +277,131 @@ class BackupRestorer( tracks: List, backupCategories: List, ): Manga { - val fetchedManga = backupManager.restoreNewManga(manga) - backupManager.restoreChapters(fetchedManga, chapters) + val fetchedManga = restoreNewManga(manga) + restoreChapters(fetchedManga, chapters) restoreExtras(fetchedManga, categories, history, tracks, backupCategories) return fetchedManga } + private suspend fun restoreChapters(manga: Manga, chapters: List) { + val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) } + + val processed = chapters.map { chapter -> + var updatedChapter = chapter + val dbChapter = dbChapters.find { it.url == updatedChapter.url } + if (dbChapter != null) { + updatedChapter = updatedChapter.copy(id = dbChapter._id) + updatedChapter = updatedChapter.copyFrom(dbChapter) + if (dbChapter.read && !updatedChapter.read) { + updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read) + } else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) { + updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read) + } + if (!updatedChapter.bookmark && dbChapter.bookmark) { + updatedChapter = updatedChapter.copy(bookmark = true) + } + } + + updatedChapter.copy(mangaId = manga.id) + } + + val newChapters = processed.groupBy { it.id > 0 } + newChapters[true]?.let { updateKnownChapters(it) } + newChapters[false]?.let { insertChapters(it) } + } + + /** + * Inserts list of chapters + */ + private suspend fun insertChapters(chapters: List) { + handler.await(true) { + chapters.forEach { chapter -> + chaptersQueries.insert( + chapter.mangaId, + chapter.url, + chapter.name, + chapter.scanlator, + chapter.read, + chapter.bookmark, + chapter.lastPageRead, + chapter.chapterNumber, + chapter.sourceOrder, + chapter.dateFetch, + chapter.dateUpload, + ) + } + } + } + + /** + * Updates a list of chapters with known database ids + */ + private suspend fun updateKnownChapters(chapters: List) { + handler.await(true) { + chapters.forEach { chapter -> + chaptersQueries.update( + mangaId = null, + url = null, + name = null, + scanlator = null, + read = chapter.read, + bookmark = chapter.bookmark, + lastPageRead = chapter.lastPageRead, + chapterNumber = null, + sourceOrder = null, + dateFetch = null, + dateUpload = null, + chapterId = chapter.id, + ) + } + } + } + + /** + * Fetches manga information + * + * @param manga manga that needs updating + * @return Updated manga info. + */ + private suspend fun restoreNewManga(manga: Manga): Manga { + return manga.copy( + initialized = manga.description != null, + id = insertManga(manga), + ) + } + + /** + * Inserts manga and returns id + * + * @return id of [Manga], null if not found + */ + private suspend fun insertManga(manga: Manga): Long { + return handler.awaitOneExecutable(true) { + mangasQueries.insert( + source = manga.source, + url = manga.url, + artist = manga.artist, + author = manga.author, + description = manga.description, + genre = manga.genre, + title = manga.title, + status = manga.status, + thumbnailUrl = manga.thumbnailUrl, + favorite = manga.favorite, + lastUpdate = manga.lastUpdate, + nextUpdate = 0L, + calculateInterval = 0L, + initialized = manga.initialized, + viewerFlags = manga.viewerFlags, + chapterFlags = manga.chapterFlags, + coverLastModified = manga.coverLastModified, + dateAdded = manga.dateAdded, + updateStrategy = manga.updateStrategy, + ) + mangasQueries.selectLastInsertedRowId() + } + } + private suspend fun restoreNewManga( backupManga: Manga, chapters: List, @@ -189,24 +410,240 @@ class BackupRestorer( tracks: List, backupCategories: List, ): Manga { - backupManager.restoreChapters(backupManga, chapters) + restoreChapters(backupManga, chapters) restoreExtras(backupManga, categories, history, tracks, backupCategories) return backupManga } private suspend fun restoreExtras(manga: Manga, categories: List, history: List, tracks: List, backupCategories: List) { - backupManager.restoreCategories(manga, categories, backupCategories) - backupManager.restoreHistory(history) - backupManager.restoreTracking(manga, tracks) + restoreCategories(manga, categories, backupCategories) + restoreHistory(history) + restoreTracking(manga, tracks) } /** - * Called to update dialog in [BackupConst] + * Restores the categories a manga is in. * - * @param progress restore progress - * @param amount total restoreAmount of manga - * @param title title of restored manga + * @param manga the manga whose categories have to be restored. + * @param categories the categories to restore. */ + private suspend fun restoreCategories(manga: Manga, categories: List, backupCategories: List) { + val dbCategories = getCategories.await() + val mangaCategoriesToUpdate = mutableListOf>() + + categories.forEach { backupCategoryOrder -> + backupCategories.firstOrNull { + it.order == backupCategoryOrder.toLong() + }?.let { backupCategory -> + dbCategories.firstOrNull { dbCategory -> + dbCategory.name == backupCategory.name + }?.let { dbCategory -> + mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id)) + } + } + } + + // Update database + if (mangaCategoriesToUpdate.isNotEmpty()) { + handler.await(true) { + mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id) + mangaCategoriesToUpdate.forEach { (mangaId, categoryId) -> + mangas_categoriesQueries.insert(mangaId, categoryId) + } + } + } + } + + /** + * Restore history from Json + * + * @param history list containing history to be restored + */ + private suspend fun restoreHistory(history: List) { + // List containing history to be updated + val toUpdate = mutableListOf() + for ((url, lastRead, readDuration) in history) { + var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) } + // Check if history already in database and update + if (dbHistory != null) { + dbHistory = dbHistory.copy( + last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)), + time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read, + ) + toUpdate.add( + HistoryUpdate( + chapterId = dbHistory.chapter_id, + readAt = dbHistory.last_read!!, + sessionReadDuration = dbHistory.time_read, + ), + ) + } else { + // If not in database create + handler + .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) } + ?.let { + toUpdate.add( + HistoryUpdate( + chapterId = it._id, + readAt = Date(lastRead), + sessionReadDuration = readDuration, + ), + ) + } + } + } + handler.await(true) { + toUpdate.forEach { payload -> + historyQueries.upsert( + payload.chapterId, + payload.readAt, + payload.sessionReadDuration, + ) + } + } + } + + /** + * Restores the sync of a manga. + * + * @param manga the manga whose sync have to be restored. + * @param tracks the track list to restore. + */ + private suspend fun restoreTracking(manga: Manga, tracks: List) { + // Get tracks from database + val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) } + val toUpdate = mutableListOf() + val toInsert = mutableListOf() + + tracks + // Fix foreign keys with the current manga id + .map { it.copy(mangaId = manga.id) } + .forEach { track -> + var isInDatabase = false + for (dbTrack in dbTracks) { + if (track.syncId == dbTrack.sync_id) { + // The sync is already in the db, only update its fields + var temp = dbTrack + if (track.remoteId != dbTrack.remote_id) { + temp = temp.copy(remote_id = track.remoteId) + } + if (track.libraryId != dbTrack.library_id) { + temp = temp.copy(library_id = track.libraryId) + } + temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead)) + isInDatabase = true + toUpdate.add(temp) + break + } + } + if (!isInDatabase) { + // Insert new sync. Let the db assign the id + toInsert.add(track.copy(id = 0)) + } + } + + // Update database + if (toUpdate.isNotEmpty()) { + handler.await(true) { + toUpdate.forEach { track -> + manga_syncQueries.update( + track.manga_id, + track.sync_id, + track.remote_id, + track.library_id, + track.title, + track.last_chapter_read, + track.total_chapters, + track.status, + track.score, + track.remote_url, + track.start_date, + track.finish_date, + track._id, + ) + } + } + } + if (toInsert.isNotEmpty()) { + handler.await(true) { + toInsert.forEach { track -> + manga_syncQueries.insert( + track.mangaId, + track.syncId, + track.remoteId, + track.libraryId, + track.title, + track.lastChapterRead, + track.totalChapters, + track.status, + track.score, + track.remoteUrl, + track.startDate, + track.finishDate, + ) + } + } + } + } + + private fun restoreAppPreferences(preferences: List) { + restorePreferences(preferences, preferenceStore) + + restoreProgress += 1 + showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.app_settings), context.getString(R.string.restoring_backup)) + } + + private fun restoreSourcePreferences(preferences: List) { + preferences.forEach { + val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) + restorePreferences(it.prefs, sourcePrefs) + } + + restoreProgress += 1 + showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.source_settings), context.getString(R.string.restoring_backup)) + } + + private fun restorePreferences( + toRestore: List, + preferenceStore: PreferenceStore, + ) { + val prefs = preferenceStore.getAll() + toRestore.forEach { (key, value) -> + when (value) { + is IntPreferenceValue -> { + if (prefs[key] is Int?) { + preferenceStore.getInt(key).set(value.value) + } + } + is LongPreferenceValue -> { + if (prefs[key] is Long?) { + preferenceStore.getLong(key).set(value.value) + } + } + is FloatPreferenceValue -> { + if (prefs[key] is Float?) { + preferenceStore.getFloat(key).set(value.value) + } + } + is StringPreferenceValue -> { + if (prefs[key] is String?) { + preferenceStore.getString(key).set(value.value) + } + } + is BooleanPreferenceValue -> { + if (prefs[key] is Boolean?) { + preferenceStore.getBoolean(key).set(value.value) + } + } + is StringSetPreferenceValue -> { + if (prefs[key] is Set<*>?) { + preferenceStore.getStringSet(key).set(value.value) + } + } + } + } + } + private fun showRestoreProgress(progress: Int, amount: Int, title: String, contentTitle: String) { notifier.showRestoreProgress(title, contentTitle, progress, amount) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index 8a9de244d..0bfe17e59 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.data.backup.models +import eu.kanade.tachiyomi.BuildConfig import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import java.text.SimpleDateFormat @@ -10,15 +11,18 @@ import java.util.Locale data class Backup( @ProtoNumber(1) val backupManga: List, @ProtoNumber(2) var backupCategories: List = emptyList(), - // Bump by 100 to specify this is a 0.x value @ProtoNumber(100) var backupBrokenSources: List = emptyList(), @ProtoNumber(101) var backupSources: List = emptyList(), + @ProtoNumber(104) var backupPreferences: List = emptyList(), + @ProtoNumber(105) var backupSourcePreferences: List = emptyList(), ) { companion object { - fun getBackupFilename(): String { + val filenameRegex = """${BuildConfig.APPLICATION_ID}_\d+-\d+-\d+_\d+-\d+.tachibk""".toRegex() + + fun getFilename(): String { val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) - return "tachiyomi_$date.proto.gz" + return "${BuildConfig.APPLICATION_ID}_$date.tachibk" } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt index 8d93a0b32..c27c86fec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupCategory.kt @@ -9,7 +9,6 @@ class BackupCategory( @ProtoNumber(1) var name: String, @ProtoNumber(2) var order: Long = 0, // @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x - // Bump by 100 to specify this is a 0.x value @ProtoNumber(100) var flags: Long = 0, ) { fun getCategory(): Category { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt new file mode 100644 index 000000000..3884f37e3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupPreference.kt @@ -0,0 +1,37 @@ +package eu.kanade.tachiyomi.data.backup.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class BackupPreference( + @ProtoNumber(1) val key: String, + @ProtoNumber(2) val value: PreferenceValue, +) + +@Serializable +data class BackupSourcePreferences( + @ProtoNumber(1) val sourceKey: String, + @ProtoNumber(2) val prefs: List, +) + +@Serializable +sealed class PreferenceValue + +@Serializable +data class IntPreferenceValue(val value: Int) : PreferenceValue() + +@Serializable +data class LongPreferenceValue(val value: Long) : PreferenceValue() + +@Serializable +data class FloatPreferenceValue(val value: Float) : PreferenceValue() + +@Serializable +data class StringPreferenceValue(val value: String) : PreferenceValue() + +@Serializable +data class BooleanPreferenceValue(val value: Boolean) : PreferenceValue() + +@Serializable +data class StringSetPreferenceValue(val value: Set) : PreferenceValue() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt index 38583ee64..3f155e7f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt @@ -8,9 +8,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.saveTo import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import logcat.LogPriority import okhttp3.Response import okio.buffer import okio.sink +import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.model.Chapter import uy.kohesive.injekt.injectLazy import java.io.File @@ -97,6 +99,7 @@ class ChapterCache(private val context: Context) { editor.commit() editor.abortUnlessCommitted() } catch (e: Exception) { + logcat(LogPriority.WARN, e) { "Failed to put page list to cache" } // Ignore. } finally { editor?.abortUnlessCommitted() @@ -174,7 +177,7 @@ class ChapterCache(private val context: Context) { * @return status of deletion for the file. */ private fun removeFileFromCache(file: String): Boolean { - // Make sure we don't delete the journal file (keeps track of cache). + // Make sure we don't delete the journal file (keeps track of cache) if (file == "journal" || file.startsWith("journal.")) { return false } @@ -182,9 +185,10 @@ class ChapterCache(private val context: Context) { return try { // Remove the extension from the file to get the key of the cache val key = file.substringBeforeLast(".") - // Remove file from cache. + // Remove file from cache diskCache.remove(key) } catch (e: Exception) { + logcat(LogPriority.WARN, e) { "Failed to remove file from cache" } false } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 4ae5cfade..9a15967f4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -43,7 +43,6 @@ import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo -import tachiyomi.core.util.lang.awaitSingle import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNow import tachiyomi.core.util.lang.withIOContext @@ -363,7 +362,7 @@ class Downloader( if (page.imageUrl.isNullOrEmpty()) { page.status = Page.State.LOAD_PAGE try { - page.imageUrl = download.source.fetchImageUrl(page).awaitSingle() + page.imageUrl = download.source.getImageUrl(page) } catch (e: Throwable) { page.status = Page.State.ERROR } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index cdca75770..651858332 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -59,9 +59,9 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD +import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.GetLibraryManga import tachiyomi.domain.manga.interactor.GetManga -import tachiyomi.domain.manga.interactor.SetFetchInterval import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.source.model.SourceNotInstalledException @@ -90,7 +90,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private val getCategories: GetCategories = Injekt.get() private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() private val refreshTracks: RefreshTracks = Injekt.get() - private val setFetchInterval: SetFetchInterval = Injekt.get() + private val fetchInterval: FetchInterval = Injekt.get() private val notifier = LibraryUpdateNotifier(context) @@ -186,7 +186,40 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet .distinctBy { it.manga.id } } + val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get() + val skippedUpdates = mutableListOf>() + val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now()) + mangaToUpdate = listToUpdate + .filter { + when { + it.manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> { + skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_always_update)) + false + } + + MANGA_NON_COMPLETED in restrictions && it.manga.status.toInt() == SManga.COMPLETED -> { + skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_completed)) + false + } + + MANGA_HAS_UNREAD in restrictions && it.unreadCount != 0L -> { + skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_caught_up)) + false + } + + MANGA_NON_READ in restrictions && it.totalChapters > 0L && !it.hasStarted -> { + skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_started)) + false + } + + MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindow.second -> { + skippedUpdates.add(it.manga to context.getString(R.string.skipped_reason_not_in_release_period)) + false + } + else -> true + } + } .sortedBy { it.manga.title } // Warn when excessively checking a single source @@ -197,6 +230,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { notifier.showQueueSizeWarningNotification() } + + if (skippedUpdates.isNotEmpty()) { + // TODO: surface skipped reasons to user? + logcat { + skippedUpdates + .groupBy { it.second } + .map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" } + .joinToString() + } + notifier.showUpdateSkippedNotification(skippedUpdates.size) + } } /** @@ -212,11 +256,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val progressCount = AtomicInteger(0) val currentlyUpdatingManga = CopyOnWriteArrayList() val newUpdates = CopyOnWriteArrayList>>() - val skippedUpdates = CopyOnWriteArrayList>() val failedUpdates = CopyOnWriteArrayList>() val hasDownloads = AtomicBoolean(false) - val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get() - val fetchWindow = setFetchInterval.getWindow(ZonedDateTime.now()) + val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now()) coroutineScope { mangaToUpdate.groupBy { it.manga.source }.values @@ -237,49 +279,30 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet progressCount, manga, ) { - when { - manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> - skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update)) + try { + val newChapters = updateManga(manga, fetchWindow) + .sortedByDescending { it.sourceOrder } - MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED -> - skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed)) - - MANGA_HAS_UNREAD in restrictions && libraryManga.unreadCount != 0L -> - skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_caught_up)) - - MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted -> - skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started)) - - MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > fetchWindow.second -> - skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period)) - - else -> { - try { - val newChapters = updateManga(manga, fetchWindow) - .sortedByDescending { it.sourceOrder } - - if (newChapters.isNotEmpty()) { - val categoryIds = getCategories.await(manga.id).map { it.id } - if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) { - downloadChapters(manga, newChapters) - hasDownloads.set(true) - } - - libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size } - - // Convert to the manga that contains new chapters - newUpdates.add(manga to newChapters.toTypedArray()) - } - } catch (e: Throwable) { - val errorMessage = when (e) { - is NoChaptersException -> context.getString(R.string.no_chapters_error) - // failedUpdates will already have the source, don't need to copy it into the message - is SourceNotInstalledException -> context.getString(R.string.loader_not_implemented_error) - else -> e.message - } - failedUpdates.add(manga to errorMessage) + if (newChapters.isNotEmpty()) { + val categoryIds = getCategories.await(manga.id).map { it.id } + if (manga.shouldDownloadNewChapters(categoryIds, downloadPreferences)) { + downloadChapters(manga, newChapters) + hasDownloads.set(true) } + + libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size } + + // Convert to the manga that contains new chapters + newUpdates.add(manga to newChapters.toTypedArray()) } + } catch (e: Throwable) { + val errorMessage = when (e) { + is NoChaptersException -> context.getString(R.string.no_chapters_error) + // failedUpdates will already have the source, don't need to copy it into the message + is SourceNotInstalledException -> context.getString(R.string.loader_not_implemented_error) + else -> e.message + } + failedUpdates.add(manga to errorMessage) } if (libraryPreferences.autoUpdateTrackers().get()) { @@ -309,16 +332,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet errorFile.getUriCompat(context), ) } - if (skippedUpdates.isNotEmpty()) { - // TODO: surface skipped reasons to user - logcat { - skippedUpdates - .groupBy { it.second } - .map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" } - .joinToString() - } - notifier.showUpdateSkippedNotification(skippedUpdates.size) - } } private fun downloadChapters(manga: Manga, chapters: List) { @@ -428,29 +441,27 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet completed: AtomicInteger, manga: Manga, block: suspend () -> Unit, - ) { - coroutineScope { - ensureActive() + ) = coroutineScope { + ensureActive() - updatingManga.add(manga) - notifier.showProgressNotification( - updatingManga, - completed.get(), - mangaToUpdate.size, - ) + updatingManga.add(manga) + notifier.showProgressNotification( + updatingManga, + completed.get(), + mangaToUpdate.size, + ) - block() + block() - ensureActive() + ensureActive() - updatingManga.remove(manga) - completed.getAndIncrement() - notifier.showProgressNotification( - updatingManga, - completed.get(), - mangaToUpdate.size, - ) - } + updatingManga.remove(manga) + completed.getAndIncrement() + notifier.showProgressNotification( + updatingManga, + completed.get(), + mangaToUpdate.size, + ) } /** @@ -497,7 +508,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private const val WORK_NAME_AUTO = "LibraryUpdate-auto" private const val WORK_NAME_MANUAL = "LibraryUpdate-manual" - private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/help/guides/troubleshooting" + private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/docs/guides/troubleshooting/" private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 0c4606a4d..833680ba4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -30,10 +30,14 @@ import tachiyomi.core.util.lang.launchUI import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.injectLazy +import java.text.NumberFormat class LibraryUpdateNotifier(private val context: Context) { private val preferences: SecurityPreferences by injectLazy() + private val percentFormatter = NumberFormat.getPercentInstance().apply { + maximumFractionDigits = 0 + } /** * Pending intent of action that cancels the library update @@ -78,7 +82,7 @@ class LibraryUpdateNotifier(private val context: Context) { } else { val updatingText = manga.joinToString("\n") { it.title.chop(40) } progressNotificationBuilder - .setContentTitle(context.getString(R.string.notification_updating, current, total)) + .setContentTitle(context.getString(R.string.notification_updating_progress, percentFormatter.format(current.toFloat() / total))) .setStyle(NotificationCompat.BigTextStyle().bigText(updatingText)) } @@ -329,11 +333,11 @@ class LibraryUpdateNotifier(private val context: Context) { } companion object { - const val HELP_WARNING_URL = "https://tachiyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads" + const val HELP_WARNING_URL = "https://tachiyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads" } } private const val NOTIF_MAX_CHAPTERS = 5 private const val NOTIF_TITLE_MAX_LEN = 45 private const val NOTIF_ICON_SIZE = 192 -private const val HELP_SKIPPED_URL = "https://tachiyomi.org/help/faq/#why-does-global-update-skip-some-entries" +private const val HELP_SKIPPED_URL = "https://tachiyomi.org/docs/faq/library#why-is-global-update-skipping-entries" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt index 9656c0fd9..783d8bdae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.saver -import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context import android.graphics.Bitmap @@ -28,30 +27,59 @@ class ImageSaver( val context: Context, ) { - @SuppressLint("InlinedApi") fun save(image: Image): Uri { val data = image.data - val type = ImageUtil.findImageType(data) ?: throw Exception("Not an image") + val type = ImageUtil.findImageType(data) ?: throw IllegalArgumentException("Not an image") val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}") if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) { return save(data(), image.location.directory(context), filename) } + return saveApi29(image, type, filename, data) + } + + private fun save(inputStream: InputStream, directory: File, filename: String): Uri { + directory.mkdirs() + + val destFile = File(directory, filename) + + inputStream.use { input -> + destFile.outputStream().use { output -> + input.copyTo(output) + } + } + + DiskUtil.scanMedia(context, destFile.toUri()) + + return destFile.getUriCompat(context) + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun saveApi29( + image: Image, + type: ImageUtil.ImageType, + filename: String, + data: () -> InputStream, + ): Uri { val pictureDir = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - val folderRelativePath = "${Environment.DIRECTORY_PICTURES}/${context.getString(R.string.app_name)}/" val imageLocation = (image.location as Location.Pictures).relativePath + val relativePath = listOf( + Environment.DIRECTORY_PICTURES, + context.getString(R.string.app_name), + imageLocation, + ).joinToString(File.separator) val contentValues = contentValuesOf( + MediaStore.Images.Media.RELATIVE_PATH to relativePath, MediaStore.Images.Media.DISPLAY_NAME to image.name, MediaStore.Images.Media.MIME_TYPE to type.mime, - MediaStore.Images.Media.RELATIVE_PATH to folderRelativePath + imageLocation, ) - val picture = findUriOrDefault(folderRelativePath, "$imageLocation$filename") { + val picture = findUriOrDefault(relativePath, filename) { context.contentResolver.insert( pictureDir, contentValues, @@ -74,49 +102,34 @@ class ImageSaver( return picture } - private fun save(inputStream: InputStream, directory: File, filename: String): Uri { - directory.mkdirs() - - val destFile = File(directory, filename) - - inputStream.use { input -> - destFile.outputStream().use { output -> - input.copyTo(output) - } - } - - DiskUtil.scanMedia(context, destFile.toUri()) - - return destFile.getUriCompat(context) - } - @RequiresApi(Build.VERSION_CODES.Q) - private fun findUriOrDefault(relativePath: String, imagePath: String, default: () -> Uri): Uri { + private fun findUriOrDefault(path: String, filename: String, default: () -> Uri): Uri { val projection = arrayOf( MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME, - MediaStore.Images.Media.MIME_TYPE, MediaStore.MediaColumns.RELATIVE_PATH, - MediaStore.MediaColumns.DATE_MODIFIED, ) val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?" + // Need to make sure it ends with the separator + val normalizedPath = "${path.removeSuffix(File.separator)}${File.separator}" + context.contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, - arrayOf(relativePath, imagePath), + arrayOf(normalizedPath, filename), null, ).use { cursor -> if (cursor != null && cursor.count >= 1) { - cursor.moveToFirst().let { + if (cursor.moveToFirst()) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) } } } + return default() } } @@ -153,19 +166,12 @@ sealed class Image( } sealed interface Location { - data class Pictures private constructor(val relativePath: String) : Location { - companion object { - fun create(relativePath: String = ""): Pictures { - return Pictures(relativePath) - } - } - } + data class Pictures(val relativePath: String) : Location data object Cache : Location fun directory(context: Context): File { return when (this) { - Cache -> context.cacheImageDir is Pictures -> { val file = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), @@ -179,6 +185,7 @@ sealed interface Location { } file } + Cache -> context.cacheImageDir } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt similarity index 56% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTrackService.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt index 7f1494707..c61c55e78 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt @@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.track import eu.kanade.tachiyomi.data.database.models.Track /** - * For track services api that support deleting a manga entry for a user's list + * Tracker that support deleting am entry from a user's list. */ -interface DeletableTrackService { +interface DeletableTracker { suspend fun delete(track: Track): Track } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTracker.kt similarity index 62% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTrackService.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTracker.kt index 75245cf80..a501cded8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/EnhancedTracker.kt @@ -6,31 +6,32 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.model.Track /** - * An Enhanced Track Service will never prompt the user to match a manga with the remote. - * It is expected that such Track Service can only work with specific sources and unique IDs. + * A tracker that will never prompt the user to manually bind an entry. + * It is expected that such tracker can only work with specific sources and unique IDs. */ -interface EnhancedTrackService { +interface EnhancedTracker { + /** - * This TrackService will only work with the sources that are accepted by this filter function. + * This tracker will only work with the sources that are accepted by this filter function. */ fun accept(source: Source): Boolean { return source::class.qualifiedName in getAcceptedSources() } /** - * Fully qualified source classes that this track service is compatible with. + * Fully qualified source classes that this tracker is compatible with. */ fun getAcceptedSources(): List fun loginNoop() /** - * match is similar to TrackService.search, but only return zero or one match. + * Similar to [Tracker].search, but only returns zero or one match. */ suspend fun match(manga: Manga): TrackSearch? /** - * Checks whether the provided source/track/manga triplet is from this TrackService + * Checks whether the provided source/track/manga triplet is from this [Tracker] */ fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt similarity index 95% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt index e30100e29..490ea0e96 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt @@ -5,7 +5,7 @@ import androidx.annotation.CallSuper import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack +import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.domain.track.service.TrackPreferences @@ -28,7 +28,7 @@ import uy.kohesive.injekt.injectLazy import java.time.ZoneOffset import tachiyomi.domain.track.model.Track as DomainTrack -abstract class TrackService(val id: Long, val name: String) { +abstract class Tracker(val id: Long, val name: String) { val trackPreferences: TrackPreferences by injectLazy() val networkService: NetworkHelper by injectLazy() @@ -83,7 +83,7 @@ abstract class TrackService(val id: Long, val name: String) { @CallSuper open fun logout() { - trackPreferences.setTrackCredentials(this, "", "") + trackPreferences.setCredentials(this, "", "") } open val isLoggedIn: Boolean @@ -95,7 +95,7 @@ abstract class TrackService(val id: Long, val name: String) { fun getPassword() = trackPreferences.trackPassword(this).get() fun saveCredentials(username: String, password: String) { - trackPreferences.setTrackCredentials(this, username, password) + trackPreferences.setCredentials(this, username, password) } // TODO: move this to an interactor, and update all trackers based on common data @@ -111,7 +111,7 @@ abstract class TrackService(val id: Long, val name: String) { insertTrack.await(track) - // TODO: merge into SyncChaptersWithTrackServiceTwoWay? + // TODO: merge into [SyncChapterProgressWithTrack]? // Update chapter progress if newer chapters marked read locally if (hasReadChapters) { val latestLocalReadChapterNumber = allChapters @@ -143,7 +143,7 @@ abstract class TrackService(val id: Long, val name: String) { } } - syncChapterProgressWithTrack.await(mangaId, track, this@TrackService) + syncChapterProgressWithTrack.await(mangaId, track, this@Tracker) } } catch (e: Throwable) { withUIContext { Injekt.get().toast(e.message) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt similarity index 51% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt index c6d62c382..3943537e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.track -import android.content.Context import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.kavita.Kavita @@ -11,33 +10,27 @@ import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi -class TrackManager(context: Context) { +class TrackerManager { companion object { - const val MYANIMELIST = 1L const val ANILIST = 2L const val KITSU = 3L - const val SHIKIMORI = 4L - const val BANGUMI = 5L - const val KOMGA = 6L - const val MANGA_UPDATES = 7L const val KAVITA = 8L - const val SUWAYOMI = 9L } - val myAnimeList = MyAnimeList(MYANIMELIST) + val myAnimeList = MyAnimeList(1L) val aniList = Anilist(ANILIST) val kitsu = Kitsu(KITSU) - val shikimori = Shikimori(SHIKIMORI) - val bangumi = Bangumi(BANGUMI) - val komga = Komga(KOMGA) - val mangaUpdates = MangaUpdates(MANGA_UPDATES) - val kavita = Kavita(context, KAVITA) - val suwayomi = Suwayomi(SUWAYOMI) + val shikimori = Shikimori(4L) + val bangumi = Bangumi(5L) + val komga = Komga(6L) + val mangaUpdates = MangaUpdates(7L) + val kavita = Kavita(KAVITA) + val suwayomi = Suwayomi(9L) - val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi) + val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi) - fun getService(id: Long) = services.find { it.id == id } + fun get(id: Long) = trackers.find { it.id == id } - fun hasLoggedServices() = services.any { it.isLoggedIn } + fun hasLoggedIn() = trackers.any { it.isLoggedIn } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index c6365106e..3ecf9119d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -4,15 +4,15 @@ import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.DeletableTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import uy.kohesive.injekt.injectLazy import tachiyomi.domain.track.model.Track as DomainTrack -class Anilist(id: Long) : TrackService(id, "AniList"), DeletableTrackService { +class Anilist(id: Long) : Tracker(id, "AniList"), DeletableTracker { companion object { const val READING = 1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index 0e39d680f..46de5e735 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.Serializable import uy.kohesive.injekt.injectLazy @@ -20,7 +20,7 @@ data class ALManga( val total_chapters: Int, ) { - fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply { + fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply { media_id = this@ALManga.media_id title = title_user_pref total_chapters = this@ALManga.total_chapters @@ -50,7 +50,7 @@ data class ALUserManga( val manga: ALManga, ) { - fun toTrack() = Track.create(TrackManager.ANILIST).apply { + fun toTrack() = Track.create(TrackerManager.ANILIST).apply { media_id = manga.media_id title = manga.title_user_pref status = toTrackStatus() @@ -62,7 +62,7 @@ data class ALUserManga( total_chapters = manga.total_chapters } - fun toTrackStatus() = when (list_status) { + private fun toTrackStatus() = when (list_status) { "CURRENT" -> Anilist.READING "COMPLETED" -> Anilist.COMPLETED "PAUSED" -> Anilist.ON_HOLD diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt index f5d8ecc0d..71aff2f2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt @@ -4,19 +4,19 @@ import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import uy.kohesive.injekt.injectLazy -class Bangumi(id: Long) : TrackService(id, "Bangumi") { +class Bangumi(id: Long) : Tracker(id, "Bangumi") { private val json: Json by injectLazy() private val interceptor by lazy { BangumiInterceptor(this) } - private val api by lazy { BangumiApi(client, interceptor) } + private val api by lazy { BangumiApi(id, client, interceptor) } override fun getScoreList(): List { return IntRange(0, 10).map(Int::toString) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt index 444487df1..6762fdcaf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.track.bangumi import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -26,7 +25,11 @@ import uy.kohesive.injekt.injectLazy import java.net.URLEncoder import java.nio.charset.StandardCharsets -class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) { +class BangumiApi( + private val trackId: Long, + private val client: OkHttpClient, + interceptor: BangumiInterceptor, +) { private val json: Json by injectLazy() @@ -105,7 +108,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept } else { 0 } - return TrackSearch.create(TrackManager.BANGUMI).apply { + return TrackSearch.create(trackId).apply { media_id = obj["id"]!!.jsonPrimitive.long title = obj["name_cn"]!!.jsonPrimitive.content cover_url = coverUrl diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt index 0a45187a3..cb6e52e68 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt @@ -1,20 +1,22 @@ package eu.kanade.tachiyomi.data.track.kavita -import android.content.Context -import android.content.SharedPreferences import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.sourcePreferences import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.source.service.SourceManager +import uy.kohesive.injekt.injectLazy import java.security.MessageDigest import tachiyomi.domain.track.model.Track as DomainTrack -class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita"), EnhancedTrackService { +class Kavita(id: Long) : Tracker(id, "Kavita"), EnhancedTracker { companion object { const val UNREAD = 1 @@ -27,6 +29,8 @@ class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita" private val interceptor by lazy { KavitaInterceptor(this) } val api by lazy { KavitaApi(client, interceptor) } + private val sourceManager: SourceManager by injectLazy() + override fun getLogo(): Int = R.drawable.ic_tracker_kavita override fun getLogoColor() = Color.rgb(74, 198, 148) @@ -83,7 +87,7 @@ class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita" saveCredentials("user", "pass") } - // TrackService.isLogged works by checking that credentials are saved. + // [Tracker].isLogged works by checking that credentials are saved. // By saving dummy, unused credentials, we can activate the tracker simply by login/logout override fun loginNoop() { saveCredentials("user", "pass") @@ -110,28 +114,29 @@ class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita" fun loadOAuth() { val oauth = OAuth() - for (sourceId in 1..3) { - val authentication = oauth.authentications[sourceId - 1] - val sourceSuffixID by lazy { - val key = "kavita_$sourceId/all/1" // Hardcoded versionID to 1 + for (id in 1..3) { + val authentication = oauth.authentications[id - 1] + val sourceId by lazy { + val key = "kavita_$id/all/1" // Hardcoded versionID to 1 val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } .reduce(Long::or) and Long.MAX_VALUE } - val preferences: SharedPreferences by lazy { - context.getSharedPreferences("source_$sourceSuffixID", 0x0000) - } - val prefApiUrl = preferences.getString("APIURL", "")!! - if (prefApiUrl.isEmpty()) { + val preferences = (sourceManager.get(sourceId) as ConfigurableSource).sourcePreferences() + + val prefApiUrl = preferences.getString("APIURL", "") + val prefApiKey = preferences.getString("APIKEY", "") + if (prefApiUrl.isNullOrEmpty() || prefApiKey.isNullOrEmpty()) { // Source not configured. Skip continue } - val prefApiKey = preferences.getString("APIKEY", "")!! + val token = api.getNewToken(apiUrl = prefApiUrl, apiKey = prefApiKey) if (token.isNullOrEmpty()) { // Source is not accessible. Skip continue } + authentication.apiUrl = prefApiUrl authentication.jwtToken = token.toString() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt index 3900c3f39..6f42f6836 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.data.track.kavita -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.Serializable @@ -22,7 +22,7 @@ data class SeriesDto( val libraryId: Int, val libraryName: String? = "", ) { - fun toTrack(): TrackSearch = TrackSearch.create(TrackManager.KAVITA).also { + fun toTrack(): TrackSearch = TrackSearch.create(TrackerManager.KAVITA).also { it.title = name it.summary = "" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index ade2456bb..a2764c685 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -4,15 +4,15 @@ import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.DeletableTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import uy.kohesive.injekt.injectLazy import java.text.DecimalFormat -class Kitsu(id: Long) : TrackService(id, "Kitsu"), DeletableTrackService { +class Kitsu(id: Long) : Tracker(id, "Kitsu"), DeletableTracker { companion object { const val READING = 1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt index 915c7ae8b..1cbf16c71 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.track.kitsu import androidx.annotation.CallSuper import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject @@ -35,7 +35,7 @@ class KitsuSearchManga(obj: JsonObject) { private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull @CallSuper - fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { + fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply { media_id = this@KitsuSearchManga.id title = canonicalTitle total_chapters = chapterCount ?: 0 @@ -67,7 +67,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) { private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int - fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { + fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply { media_id = libraryId title = canonicalTitle total_chapters = chapterCount ?: 0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt index 8937bd21a..450e502be 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt @@ -4,8 +4,8 @@ import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.source.Source import okhttp3.Dns @@ -13,7 +13,7 @@ import okhttp3.OkHttpClient import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.model.Track as DomainTrack -class Komga(id: Long) : TrackService(id, "Komga"), EnhancedTrackService { +class Komga(id: Long) : Tracker(id, "Komga"), EnhancedTracker { companion object { const val UNREAD = 1 @@ -26,7 +26,7 @@ class Komga(id: Long) : TrackService(id, "Komga"), EnhancedTrackService { .dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing .build() - val api by lazy { KomgaApi(client) } + val api by lazy { KomgaApi(id, client) } override fun getLogo() = R.drawable.ic_tracker_komga @@ -85,7 +85,7 @@ class Komga(id: Long) : TrackService(id, "Komga"), EnhancedTrackService { saveCredentials("user", "pass") } - // TrackService.isLogged works by checking that credentials are saved. + // [Tracker].isLogged works by checking that credentials are saved. // By saving dummy, unused credentials, we can activate the tracker simply by login/logout override fun loginNoop() { saveCredentials("user", "pass") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt index 89e7fafbd..5992727ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.track.komga import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.awaitSuccess @@ -19,7 +18,10 @@ import uy.kohesive.injekt.injectLazy private const val READLIST_API = "/api/v1/readlists" -class KomgaApi(private val client: OkHttpClient) { +class KomgaApi( + private val trackId: Long, + private val client: OkHttpClient, +) { private val json: Json by injectLazy() @@ -85,13 +87,13 @@ class KomgaApi(private val client: OkHttpClient) { return getTrackSearch(track.tracking_url) } - private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also { + private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(trackId).also { it.title = metadata.title it.summary = metadata.summary it.publishing_status = metadata.status } - private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also { + private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(trackId).also { it.title = name } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt index 33df93222..417f6acb8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt @@ -4,13 +4,13 @@ import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.DeletableTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch -class MangaUpdates(id: Long) : TrackService(id, "MangaUpdates"), DeletableTrackService { +class MangaUpdates(id: Long) : Tracker(id, "MangaUpdates"), DeletableTracker { companion object { const val READING_LIST = 0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 3acbb035d..cd0d1eb1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -4,14 +4,14 @@ import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.DeletableTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import uy.kohesive.injekt.injectLazy -class MyAnimeList(id: Long) : TrackService(id, "MyAnimeList"), DeletableTrackService { +class MyAnimeList(id: Long) : Tracker(id, "MyAnimeList"), DeletableTracker { companion object { const val READING = 1 @@ -28,7 +28,7 @@ class MyAnimeList(id: Long) : TrackService(id, "MyAnimeList"), DeletableTrackSer private val json: Json by injectLazy() private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) } - private val api by lazy { MyAnimeListApi(client, interceptor) } + private val api by lazy { MyAnimeListApi(id, client, interceptor) } override val supportsReadingDates: Boolean = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 1bfb4e581..d66a4c561 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.track.myanimelist import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST @@ -32,7 +31,11 @@ import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale -class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) { +class MyAnimeListApi( + private val trackId: Long, + private val client: OkHttpClient, + interceptor: MyAnimeListInterceptor, +) { private val json: Json by injectLazy() @@ -106,7 +109,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .parseAs() .let { val obj = it.jsonObject - TrackSearch.create(TrackManager.MYANIMELIST).apply { + TrackSearch.create(trackId).apply { media_id = obj["id"]!!.jsonPrimitive.long title = obj["title"]!!.jsonPrimitive.content summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt index 463059055..312c9acbd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt @@ -4,14 +4,14 @@ import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.DeletableTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import uy.kohesive.injekt.injectLazy -class Shikimori(id: Long) : TrackService(id, "Shikimori"), DeletableTrackService { +class Shikimori(id: Long) : Tracker(id, "Shikimori"), DeletableTracker { companion object { const val READING = 1 @@ -26,7 +26,7 @@ class Shikimori(id: Long) : TrackService(id, "Shikimori"), DeletableTrackService private val interceptor by lazy { ShikimoriInterceptor(this) } - private val api by lazy { ShikimoriApi(client, interceptor) } + private val api by lazy { ShikimoriApi(id, client, interceptor) } override fun getScoreList(): List { return IntRange(0, 10).map(Int::toString) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 0137ed6df..fd62f18fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.track.shikimori import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.GET @@ -28,7 +27,11 @@ import okhttp3.RequestBody.Companion.toRequestBody import tachiyomi.core.util.lang.withIOContext import uy.kohesive.injekt.injectLazy -class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) { +class ShikimoriApi( + private val trackId: Long, + private val client: OkHttpClient, + interceptor: ShikimoriInterceptor, +) { private val json: Json by injectLazy() @@ -96,7 +99,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter } private fun jsonToSearch(obj: JsonObject): TrackSearch { - return TrackSearch.create(TrackManager.SHIKIMORI).apply { + return TrackSearch.create(trackId).apply { media_id = obj["id"]!!.jsonPrimitive.long title = obj["name"]!!.jsonPrimitive.content total_chapters = obj["chapters"]!!.jsonPrimitive.int @@ -110,7 +113,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter } private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track { - return Track.create(TrackManager.SHIKIMORI).apply { + return Track.create(trackId).apply { title = mangas["name"]!!.jsonPrimitive.content media_id = obj["id"]!!.jsonPrimitive.long total_chapters = mangas["chapters"]!!.jsonPrimitive.int diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt index 3958bfa2e..2c7fd67c4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt @@ -4,16 +4,16 @@ import android.graphics.Color import androidx.annotation.StringRes import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.source.Source import tachiyomi.domain.manga.model.Manga as DomainManga import tachiyomi.domain.track.model.Track as DomainTrack -class Suwayomi(id: Long) : TrackService(id, "Suwayomi"), EnhancedTrackService { +class Suwayomi(id: Long) : Tracker(id, "Suwayomi"), EnhancedTracker { - val api by lazy { TachideskApi() } + val api by lazy { SuwayomiApi(id) } override fun getLogo() = R.drawable.ic_tracker_suwayomi diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt similarity index 87% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt index 24127c63a..5c5367bec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.data.track.suwayomi import android.app.Application +import android.content.Context import android.content.SharedPreferences import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper @@ -23,7 +23,7 @@ import uy.kohesive.injekt.injectLazy import java.nio.charset.Charset import java.security.MessageDigest -class TachideskApi { +class SuwayomiApi(private val trackId: Long) { private val network: NetworkHelper by injectLazy() private val json: Json by injectLazy() @@ -61,7 +61,7 @@ class TachideskApi { .parseAs() } - TrackSearch.create(TrackManager.SUWAYOMI).apply { + TrackSearch.create(trackId).apply { title = manga.title cover_url = "$url/thumbnail" summary = manga.description.orEmpty() @@ -100,26 +100,24 @@ class TachideskApi { return getTrackSearch(track.tracking_url) } - private val tachideskExtensionId by lazy { + private val sourceId by lazy { val key = "tachidesk/en/1" val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE } private val preferences: SharedPreferences by lazy { - Injekt.get().getSharedPreferences("source_$tachideskExtensionId", 0x0000) + Injekt.get().getSharedPreferences("source_$sourceId", Context.MODE_PRIVATE) } private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!! private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!! private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!! - - companion object { - private const val ADDRESS_TITLE = "Server URL Address" - private const val ADDRESS_DEFAULT = "" - private const val LOGIN_TITLE = "Login (Basic Auth)" - private const val LOGIN_DEFAULT = "" - private const val PASSWORD_TITLE = "Password (Basic Auth)" - private const val PASSWORD_DEFAULT = "" - } } + +private const val ADDRESS_TITLE = "Server URL Address" +private const val ADDRESS_DEFAULT = "" +private const val LOGIN_TITLE = "Login (Basic Auth)" +private const val LOGIN_DEFAULT = "" +private const val PASSWORD_TITLE = "Password (Basic Auth)" +private const val PASSWORD_DEFAULT = "" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskDto.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/TachideskDto.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt index 7c0794bc7..728967654 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt @@ -143,7 +143,7 @@ internal class AppUpdateNotifier(private val context: Context) { setContentTitle(context.getString(R.string.update_check_notification_update_available)) setContentText(context.getString(R.string.update_check_fdroid_migration_info)) setSmallIcon(R.drawable.ic_tachi) - setContentIntent(NotificationHandler.openUrl(context, "https://tachiyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version")) + setContentIntent(NotificationHandler.openUrl(context, "https://tachiyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds")) } notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt index 0b6b3e724..fc8d0f46c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageInstaller import android.os.Build +import androidx.core.content.ContextCompat import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.util.lang.use import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat @@ -100,7 +101,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic } init { - service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION)) + ContextCompat.registerReceiver(service, packageActionReceiver, IntentFilter(INSTALL_ACTION), ContextCompat.RECEIVER_EXPORTED) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 5ea3a3ee6..01f212d8e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -264,7 +264,7 @@ internal class ExtensionInstaller(private val context: Context) { isRegistered = true val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) - context.registerReceiver(this, filter) + ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt index 8f4faaaa9..7be9c4d20 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt @@ -10,8 +10,6 @@ import uy.kohesive.injekt.api.get fun Source.icon(): Drawable? = Injekt.get().getAppIconForSource(this.id) -fun Source.getPreferenceKey(): String = "source_$id" - fun Source.toStubSource(): StubSource = StubSource(id = id, lang = lang, name = name) fun Source.getNameForMangaInfo(): String { 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 1ad0faec4..c88ea5b42 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 @@ -102,7 +102,7 @@ class ExtensionDetailsScreenModel( val extension = state.value.extension ?: return "" if (!extension.hasReadme) { - return "https://tachiyomi.org/help/faq/#extensions" + return "https://tachiyomi.org/docs/faq/browse/extensions" } val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt index 78aa37109..4e391cb3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt @@ -39,7 +39,7 @@ import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.getPreferenceKey +import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito import tachiyomi.domain.source.service.SourceManager import tachiyomi.presentation.core.components.material.Scaffold @@ -134,12 +134,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { private fun populateScreen(): PreferenceScreen { val sourceId = requireArguments().getLong(SOURCE_ID) - val source = Injekt.get().get(sourceId)!! + val source = Injekt.get().get(sourceId)!! as ConfigurableSource - check(source is ConfigurableSource) - - val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) - val dataStore = SharedPreferencesDataStore(sharedPreferences) + val dataStore = SharedPreferencesDataStore(source.sourcePreferences()) preferenceManager.preferenceDataStore = dataStore val sourceScreen = preferenceManager.createPreferenceScreen(requireContext()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt index ab1431fe8..ac8964412 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt @@ -35,8 +35,8 @@ import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags @@ -177,7 +177,7 @@ internal class MigrateDialogScreenModel( } private val enhancedServices by lazy { - Injekt.get().services.filterIsInstance() + Injekt.get().trackers.filterIsInstance() } suspend fun migrateManga( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt index 14dcd9756..fe53ccfad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt @@ -31,7 +31,7 @@ fun Screen.migrateSourceTab(): TabContent { title = stringResource(R.string.migration_help_guide), icon = Icons.Outlined.HelpOutline, onClick = { - uriHandler.openUri("https://tachiyomi.org/help/guides/source-migration/") + uriHandler.openUri("https://tachiyomi.org/docs/guides/source-migration") }, ), ), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index ff6ad995b..b99a67608 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -9,39 +9,33 @@ import androidx.compose.ui.unit.dp import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import androidx.paging.filter import androidx.paging.map import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.coroutineScope import eu.kanade.core.preference.asState import eu.kanade.domain.base.BasePreferences -import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.domain.track.model.toDomainTrack +import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.util.removeCovers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import logcat.LogPriority import tachiyomi.core.preference.CheckboxState import tachiyomi.core.preference.mapAsCheckboxState import tachiyomi.core.util.lang.launchIO -import tachiyomi.core.util.system.logcat import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category @@ -54,7 +48,6 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.source.interactor.GetRemoteManga import tachiyomi.domain.source.service.SourceManager -import tachiyomi.domain.track.interactor.InsertTrack import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Date @@ -76,12 +69,9 @@ class BrowseSourceScreenModel( private val getManga: GetManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(), - private val insertTrack: InsertTrack = Injekt.get(), - private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack = Injekt.get(), + private val addTracks: AddTracks = Injekt.get(), ) : StateScreenModel(State(Listing.valueOf(listingQuery))) { - private val loggedServices by lazy { Injekt.get().services.filter { it.isLoggedIn } } - var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope) val source = sourceManager.getOrStub(sourceId) @@ -113,25 +103,20 @@ class BrowseSourceScreenModel( /** * Flow of Pager flow tied to [State.listing] */ + private val hideInLibraryItems = sourcePreferences.hideInLibraryItems().get() val mangaPagerFlowFlow = state.map { it.listing } .distinctUntilChanged() .map { listing -> - Pager( - PagingConfig(pageSize = 25), - ) { + Pager(PagingConfig(pageSize = 25)) { getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters) }.flow.map { pagingData -> pagingData.map { networkToLocalManga.await(it.toDomainManga(sourceId)) - .let { localManga -> - getManga.subscribe(localManga.url, localManga.source) - } + .let { localManga -> getManga.subscribe(localManga.url, localManga.source) } .filterNotNull() - .filter { localManga -> - !sourcePreferences.hideInLibraryItems().get() || !localManga.favorite - } .stateIn(ioCoroutineScope) } + .filter { !hideInLibraryItems || !it.value.favorite } } .cachedIn(ioCoroutineScope) } @@ -248,8 +233,7 @@ class BrowseSourceScreenModel( new = new.removeCovers(coverCache) } else { setMangaDefaultChapterFlags.await(manga) - - autoAddTrack(manga) + addTracks.bindEnhancedTracks(manga, source) } updateManga.await(new.toMangaUpdate()) @@ -286,25 +270,6 @@ class BrowseSourceScreenModel( } } - private suspend fun autoAddTrack(manga: Manga) { - loggedServices - .filterIsInstance() - .filter { it.accept(source) } - .forEach { service -> - try { - service.match(manga)?.let { track -> - track.manga_id = manga.id - (service as TrackService).bind(track) - insertTrack.await(track.toDomainTrack()!!) - - syncChapterProgressWithTrack.await(manga.id, track.toDomainTrack()!!, service) - } - } catch (e: Exception) { - logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" } - } - } - } - /** * Get user categories. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt index f82f800ae..5ec0769dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt @@ -64,7 +64,7 @@ fun SourceFilterDialog( Button(onClick = { onFilter() onDismissRequest() - },) { + }) { Text(stringResource(R.string.action_filter)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 55c2e151b..276af54ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import tachiyomi.core.util.lang.awaitSingle import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga @@ -140,7 +139,7 @@ abstract class SearchScreenModel( try { val page = withContext(coroutineDispatcher) { - source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle() + source.getSearchManga(1, query, source.getFilterList()) } val titles = page.mangas.map { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt index 77628a415..bc9fbd4a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt @@ -12,6 +12,7 @@ import eu.kanade.presentation.category.CategoryScreen import eu.kanade.presentation.category.components.CategoryCreateDialog import eu.kanade.presentation.category.components.CategoryDeleteDialog import eu.kanade.presentation.category.components.CategoryRenameDialog +import eu.kanade.presentation.category.components.CategorySortAlphabeticallyDialog import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.flow.collectLatest @@ -37,6 +38,7 @@ class CategoryScreen : Screen() { CategoryScreen( state = successState, onClickCreate = { screenModel.showDialog(CategoryDialog.Create) }, + onClickSortAlphabetically = { screenModel.showDialog(CategoryDialog.SortAlphabetically) }, onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) }, onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) }, onClickMoveUp = screenModel::moveUp, @@ -68,6 +70,12 @@ class CategoryScreen : Screen() { category = dialog.category, ) } + is CategoryDialog.SortAlphabetically -> { + CategorySortAlphabeticallyDialog( + onDismissRequest = screenModel::dismissDialog, + onSort = { screenModel.sortAlphabetically() }, + ) + } } LaunchedEffect(Unit) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt index 45fbdcb4a..3a04a3475 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt @@ -61,6 +61,15 @@ class CategoryScreenModel( } } + fun sortAlphabetically() { + coroutineScope.launch { + when (reorderCategory.sortAlphabetically()) { + is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) + else -> {} + } + } + } + fun moveUp(category: Category) { coroutineScope.launch { when (reorderCategory.moveUp(category)) { @@ -109,6 +118,7 @@ class CategoryScreenModel( sealed interface CategoryDialog { data object Create : CategoryDialog + data object SortAlphabetically : CategoryDialog data class Rename(val category: Category) : CategoryDialog data class Delete(val category: Category) : CategoryDialog } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt index 143185634..dcbb4536f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt @@ -1,13 +1,13 @@ package eu.kanade.tachiyomi.ui.download import android.view.LayoutInflater -import android.view.ViewGroup import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow @@ -42,7 +42,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.ViewCompat -import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager import cafe.adriel.voyager.core.model.rememberScreenModel @@ -243,6 +242,7 @@ object DownloadQueueScreen : Screen() { ) return@Scaffold } + val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() } @@ -252,13 +252,13 @@ object DownloadQueueScreen : Screen() { Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) { AndroidView( + modifier = Modifier.fillMaxWidth(), factory = { context -> screenModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context)) screenModel.adapter = DownloadAdapter(screenModel.listener) - screenModel.controllerBinding.recycler.adapter = screenModel.adapter + screenModel.controllerBinding.root.adapter = screenModel.adapter screenModel.adapter?.isHandleDragEnabled = true - screenModel.adapter?.fastScroller = screenModel.controllerBinding.fastScroller - screenModel.controllerBinding.recycler.layoutManager = LinearLayoutManager(context) + screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(context) ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true) @@ -274,7 +274,7 @@ object DownloadQueueScreen : Screen() { screenModel.controllerBinding.root }, update = { - screenModel.controllerBinding.recycler + screenModel.controllerBinding.root .updatePadding( left = left, top = top, @@ -282,14 +282,6 @@ object DownloadQueueScreen : Screen() { bottom = bottom, ) - screenModel.controllerBinding.fastScroller - .updateLayoutParams { - leftMargin = left - topMargin = top - rightMargin = right - bottomMargin = bottom - } - screenModel.adapter?.updateDataSet(downloadList) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt index 711985887..18ab4c36b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreenModel.kt @@ -84,13 +84,17 @@ class DownloadQueueScreenModel( } reorder(newDownloads) } - R.id.move_to_top_series -> { + R.id.move_to_top_series, R.id.move_to_bottom_series -> { val (selectedSeries, otherSeries) = adapter?.currentItems ?.filterIsInstance() ?.map(DownloadItem::download) ?.partition { item.download.manga.id == it.manga.id } ?: Pair(emptyList(), emptyList()) - reorder(selectedSeries + otherSeries) + if (menuItem.itemId == R.id.move_to_top_series) { + reorder(selectedSeries + otherSeries) + } else { + reorder(otherSeries + selectedSeries) + } } R.id.cancel_download -> { cancel(listOf(item.download)) @@ -258,6 +262,6 @@ class DownloadQueueScreenModel( * @return the holder of the download or null if it's not bound. */ private fun getHolder(download: Download): DownloadHolder? { - return controllerBinding.recycler.findViewHolderForItemId(download.chapter.id) as? DownloadHolder + return controllerBinding.root.findViewHolderForItemId(download.chapter.id) as? DownloadHolder } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index a6a08ff8c..2306216a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt @@ -116,7 +116,7 @@ object HistoryTab : Tab { } } - suspend fun openChapter(context: Context, chapter: Chapter?) { + private suspend fun openChapter(context: Context, chapter: Chapter?) { if (chapter != null) { val intent = ReaderActivity.newIntent(context, chapter.mangaId, chapter.id) context.startActivity(intent) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 99b964cae..3f0408094 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -23,7 +23,7 @@ import eu.kanade.presentation.manga.DownloadAction import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.chapter.getNextUnread @@ -88,7 +88,7 @@ class LibraryScreenModel( private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val downloadCache: DownloadCache = Injekt.get(), - private val trackManager: TrackManager = Injekt.get(), + private val trackerManager: TrackerManager = Injekt.get(), ) : StateScreenModel(State()) { var activeCategoryIndex: Int by libraryPreferences.lastUsedCategory().asState(coroutineScope) @@ -101,9 +101,9 @@ class LibraryScreenModel( getTracksPerManga.subscribe(), getTrackingFilterFlow(), downloadCache.changes, - ) { searchQuery, library, tracks, loggedInTrackServices, _ -> + ) { searchQuery, library, tracks, loggedInTrackers, _ -> library - .applyFilters(tracks, loggedInTrackServices) + .applyFilters(tracks, loggedInTrackers) .applySort() .mapValues { (_, value) -> if (searchQuery != null) { @@ -169,7 +169,7 @@ class LibraryScreenModel( */ private suspend fun LibraryMap.applyFilters( trackMap: Map>, - loggedInTrackServices: Map, + loggedInTrackers: Map, ): LibraryMap { val prefs = getLibraryItemPreferencesFlow().first() val downloadedOnly = prefs.globalFilterDownloaded @@ -180,10 +180,10 @@ class LibraryScreenModel( val filterBookmarked = prefs.filterBookmarked val filterCompleted = prefs.filterCompleted - val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty() + val isNotLoggedInAnyTrack = loggedInTrackers.isEmpty() - val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } - val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } + val excludedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } + val includedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() val filterFnDownloaded: (LibraryItem) -> Boolean = { @@ -366,14 +366,14 @@ class LibraryScreenModel( * @return map of track id with the filter value */ private fun getTrackingFilterFlow(): Flow> { - val loggedServices = trackManager.services.filter { it.isLoggedIn } - return if (loggedServices.isNotEmpty()) { - val prefFlows = loggedServices + val loggedInTrackers = trackerManager.trackers.filter { it.isLoggedIn } + return if (loggedInTrackers.isNotEmpty()) { + val prefFlows = loggedInTrackers .map { libraryPreferences.filterTracking(it.id.toInt()).changes() } .toTypedArray() combine(*prefFlows) { - loggedServices - .mapIndexed { index, trackService -> trackService.id to it[index] } + loggedInTrackers + .mapIndexed { index, tracker -> tracker.id to it[index] } .toMap() } } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt index 1a9ce4208..2a59af85c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.library import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.coroutineScope import eu.kanade.domain.base.BasePreferences -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import tachiyomi.core.preference.Preference import tachiyomi.core.preference.TriState import tachiyomi.core.preference.getAndSet @@ -22,11 +22,11 @@ class LibrarySettingsScreenModel( val libraryPreferences: LibraryPreferences = Injekt.get(), private val setDisplayMode: SetDisplayMode = Injekt.get(), private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(), - private val trackManager: TrackManager = Injekt.get(), + private val trackerManager: TrackerManager = Injekt.get(), ) : ScreenModel { - val trackServices - get() = trackManager.services.filter { it.isLoggedIn } + val trackers + get() = trackerManager.trackers.filter { it.isLoggedIn } fun toggleFilter(preference: (LibraryPreferences) -> Preference) { preference(libraryPreferences).getAndSet { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index cde9762d5..5469a11a8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -158,7 +158,7 @@ object LibraryTab : Tab { EmptyScreenAction( stringResId = R.string.getting_started_guide, icon = Icons.Outlined.HelpOutline, - onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") }, + onClick = { handler.openUri("https://tachiyomi.org/docs/guides/getting-started") }, ), ), ) 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 868a8cee7..7b07c8521 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 @@ -138,7 +138,7 @@ class MainActivity : BaseActivity() { libraryPreferences = libraryPreferences, readerPreferences = Injekt.get(), backupPreferences = Injekt.get(), - trackManager = Injekt.get(), + trackerManager = Injekt.get(), ) } else { false @@ -151,6 +151,7 @@ class MainActivity : BaseActivity() { } // Draw edge-to-edge + // TODO: replace with ComponentActivity#enableEdgeToEdge WindowCompat.setDecorFitsSystemWindows(window, false) setComposeContent { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt index ffb6553c4..784a99aa2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt @@ -102,8 +102,8 @@ class MangaCoverScreenModel( imageSaver.save( Image.Cover( bitmap = bitmap, - name = manga.title, - location = if (temp) Location.Cache else Location.Pictures.create(), + name = "cover", + location = if (temp) Location.Cache else Location.Pictures(manga.title), ), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 8f56c4fcb..39595fbe5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -99,6 +99,7 @@ class MangaScreen( MangaScreen( state = successState, snackbarHostState = screenModel.snackbarHostState, + dateRelativeTime = screenModel.relativeTime, dateFormat = screenModel.dateFormat, fetchInterval = successState.manga.fetchInterval, isTabletUi = isTabletUi(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 71b7c4bf7..8d1bace7d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -15,6 +15,7 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.downloadedFilter import eu.kanade.domain.manga.model.toSManga +import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction @@ -23,9 +24,8 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.manga.track.TrackItem @@ -84,7 +84,7 @@ class MangaScreenModel( private val libraryPreferences: LibraryPreferences = Injekt.get(), readerPreferences: ReaderPreferences = Injekt.get(), uiPreferences: UiPreferences = Injekt.get(), - private val trackManager: TrackManager = Injekt.get(), + private val trackerManager: TrackerManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val downloadCache: DownloadCache = Injekt.get(), private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(), @@ -97,6 +97,7 @@ class MangaScreenModel( private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), + private val addTracks: AddTracks = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), private val mangaRepository: MangaRepository = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), @@ -105,7 +106,7 @@ class MangaScreenModel( private val successState: State.Success? get() = state.value as? State.Success - private val loggedServices by lazy { trackManager.services.filter { it.isLoggedIn } } + private val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } } val manga: Manga? get() = successState?.manga @@ -125,6 +126,7 @@ class MangaScreenModel( val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get() val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get() + val relativeTime by uiPreferences.relativeTime().asState(coroutineScope) val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope) @@ -314,24 +316,7 @@ class MangaScreenModel( } // Finally match with enhanced tracking when available - val source = state.source - state.trackItems - .map { it.service } - .filterIsInstance() - .filter { it.accept(source) } - .forEach { service -> - launchIO { - try { - service.match(manga)?.let { track -> - (service as TrackService).register(track, mangaId) - } - } catch (e: Exception) { - logcat(LogPriority.WARN, e) { - "Could not match manga: ${manga.title} with service $service" - } - } - } - } + addTracks.bindEnhancedTracks(manga, state.source) } } } @@ -948,11 +933,11 @@ class MangaScreenModel( getTracks.subscribe(manga.id) .catch { logcat(LogPriority.ERROR, it) } .map { tracks -> - loggedServices + loggedInTrackers // Map to TrackItem .map { service -> TrackItem(tracks.find { it.syncId == service.id }, service) } // Show only if the service supports this manga's source - .filter { (it.service as? EnhancedTrackService)?.accept(source!!) ?: true } + .filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true } } .distinctUntilChanged() .collectLatest { trackItems -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index 47d7c5132..f25dab5c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -46,14 +46,14 @@ import eu.kanade.presentation.track.TrackChapterSelector import eu.kanade.presentation.track.TrackDateSelector import eu.kanade.presentation.track.TrackInfoDialogHome import eu.kanade.presentation.track.TrackScoreSelector -import eu.kanade.presentation.track.TrackServiceSearch import eu.kanade.presentation.track.TrackStatusSelector +import eu.kanade.presentation.track.TrackerSearch import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.DeletableTrackService -import eu.kanade.tachiyomi.data.track.EnhancedTrackService -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.Tracker +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import eu.kanade.tachiyomi.util.system.openInBrowser @@ -105,7 +105,7 @@ data class TrackInfoDialogHomeScreen( navigator.push( TrackStatusSelectorScreen( track = it.track!!, - serviceId = it.service.id, + serviceId = it.tracker.id, ), ) }, @@ -113,7 +113,7 @@ data class TrackInfoDialogHomeScreen( navigator.push( TrackChapterSelectorScreen( track = it.track!!, - serviceId = it.service.id, + serviceId = it.tracker.id, ), ) }, @@ -121,7 +121,7 @@ data class TrackInfoDialogHomeScreen( navigator.push( TrackScoreSelectorScreen( track = it.track!!, - serviceId = it.service.id, + serviceId = it.tracker.id, ), ) }, @@ -129,7 +129,7 @@ data class TrackInfoDialogHomeScreen( navigator.push( TrackDateSelectorScreen( track = it.track!!, - serviceId = it.service.id, + serviceId = it.tracker.id, start = true, ), ) @@ -138,21 +138,21 @@ data class TrackInfoDialogHomeScreen( navigator.push( TrackDateSelectorScreen( track = it.track!!, - serviceId = it.service.id, + serviceId = it.tracker.id, start = false, ), ) }, onNewSearch = { - if (it.service is EnhancedTrackService) { + if (it.tracker is EnhancedTracker) { sm.registerEnhancedTracking(it) } else { navigator.push( - TrackServiceSearchScreen( + TrackerSearchScreen( mangaId = mangaId, initialQuery = it.track?.title ?: mangaTitle, currentUrl = it.track?.remoteUrl, - serviceId = it.service.id, + serviceId = it.tracker.id, ), ) } @@ -160,10 +160,10 @@ data class TrackInfoDialogHomeScreen( onOpenInBrowser = { openTrackerInBrowser(context, it) }, onRemoved = { navigator.push( - TrackServiceRemoveScreen( + TrackerRemoveScreen( mangaId = mangaId, track = it.track!!, - serviceId = it.service.id, + serviceId = it.tracker.id, ), ) }, @@ -201,12 +201,12 @@ data class TrackInfoDialogHomeScreen( } fun registerEnhancedTracking(item: TrackItem) { - item.service as EnhancedTrackService + item.tracker as EnhancedTracker coroutineScope.launchNonCancellable { val manga = Injekt.get().await(mangaId) ?: return@launchNonCancellable try { - val matchResult = item.service.match(manga) ?: throw Exception() - item.service.register(matchResult, mangaId) + val matchResult = item.tracker.match(manga) ?: throw Exception() + item.tracker.register(matchResult, mangaId) } catch (e: Exception) { withUIContext { Injekt.get().toast(R.string.error_no_match) } } @@ -236,13 +236,13 @@ data class TrackInfoDialogHomeScreen( } private fun List.mapToTrackItem(): List { - val loggedServices = Injekt.get().services.filter { it.isLoggedIn } + val loggedInTrackers = Injekt.get().trackers.filter { it.isLoggedIn } val source = Injekt.get().getOrStub(sourceId) - return loggedServices + return loggedInTrackers // Map to TrackItem .map { service -> TrackItem(find { it.syncId == service.id }, service) } // Show only if the service supports this manga's source - .filter { (it.service as? EnhancedTrackService)?.accept(source) ?: true } + .filter { (it.tracker as? EnhancedTracker)?.accept(source) ?: true } } @Immutable @@ -263,7 +263,7 @@ private data class TrackStatusSelectorScreen( val sm = rememberScreenModel { Model( track = track, - service = Injekt.get().getService(serviceId)!!, + tracker = Injekt.get().get(serviceId)!!, ) } val state by sm.state.collectAsState() @@ -271,18 +271,21 @@ private data class TrackStatusSelectorScreen( selection = state.selection, onSelectionChange = sm::setSelection, selections = remember { sm.getSelections() }, - onConfirm = { sm.setStatus(); navigator.pop() }, + onConfirm = { + sm.setStatus() + navigator.pop() + }, onDismissRequest = navigator::pop, ) } private class Model( private val track: Track, - private val service: TrackService, + private val tracker: Tracker, ) : StateScreenModel(State(track.status.toInt())) { fun getSelections(): Map { - return service.getStatusList().associateWith { service.getStatus(it) } + return tracker.getStatusList().associateWith { tracker.getStatus(it) } } fun setSelection(selection: Int) { @@ -291,7 +294,7 @@ private data class TrackStatusSelectorScreen( fun setStatus() { coroutineScope.launchNonCancellable { - service.setRemoteStatus(track.toDbTrack(), state.value.selection) + tracker.setRemoteStatus(track.toDbTrack(), state.value.selection) } } @@ -313,7 +316,7 @@ private data class TrackChapterSelectorScreen( val sm = rememberScreenModel { Model( track = track, - service = Injekt.get().getService(serviceId)!!, + tracker = Injekt.get().get(serviceId)!!, ) } val state by sm.state.collectAsState() @@ -322,14 +325,17 @@ private data class TrackChapterSelectorScreen( selection = state.selection, onSelectionChange = sm::setSelection, range = remember { sm.getRange() }, - onConfirm = { sm.setChapter(); navigator.pop() }, + onConfirm = { + sm.setChapter() + navigator.pop() + }, onDismissRequest = navigator::pop, ) } private class Model( private val track: Track, - private val service: TrackService, + private val tracker: Tracker, ) : StateScreenModel(State(track.lastChapterRead.toInt())) { fun getRange(): Iterable { @@ -347,7 +353,7 @@ private data class TrackChapterSelectorScreen( fun setChapter() { coroutineScope.launchNonCancellable { - service.setRemoteLastChapterRead(track.toDbTrack(), state.value.selection) + tracker.setRemoteLastChapterRead(track.toDbTrack(), state.value.selection) } } @@ -369,7 +375,7 @@ private data class TrackScoreSelectorScreen( val sm = rememberScreenModel { Model( track = track, - service = Injekt.get().getService(serviceId)!!, + tracker = Injekt.get().get(serviceId)!!, ) } val state by sm.state.collectAsState() @@ -378,18 +384,21 @@ private data class TrackScoreSelectorScreen( selection = state.selection, onSelectionChange = sm::setSelection, selections = remember { sm.getSelections() }, - onConfirm = { sm.setScore(); navigator.pop() }, + onConfirm = { + sm.setScore() + navigator.pop() + }, onDismissRequest = navigator::pop, ) } private class Model( private val track: Track, - private val service: TrackService, - ) : StateScreenModel(State(service.displayScore(track.toDbTrack()))) { + private val tracker: Tracker, + ) : StateScreenModel(State(tracker.displayScore(track.toDbTrack()))) { fun getSelections(): List { - return service.getScoreList() + return tracker.getScoreList() } fun setSelection(selection: String) { @@ -398,7 +407,7 @@ private data class TrackScoreSelectorScreen( fun setScore() { coroutineScope.launchNonCancellable { - service.setRemoteScore(track.toDbTrack(), state.value.selection) + tracker.setRemoteScore(track.toDbTrack(), state.value.selection) } } @@ -477,7 +486,7 @@ private data class TrackDateSelectorScreen( val sm = rememberScreenModel { Model( track = track, - service = Injekt.get().getService(serviceId)!!, + tracker = Injekt.get().get(serviceId)!!, start = start, ) } @@ -495,7 +504,10 @@ private data class TrackDateSelectorScreen( }, initialSelectedDateMillis = sm.initialSelection, selectableDates = selectableDates, - onConfirm = { sm.setDate(it); navigator.pop() }, + onConfirm = { + sm.setDate(it) + navigator.pop() + }, onRemove = { sm.confirmRemoveDate(navigator) }.takeIf { canRemove }, onDismissRequest = navigator::pop, ) @@ -503,7 +515,7 @@ private data class TrackDateSelectorScreen( private class Model( private val track: Track, - private val service: TrackService, + private val tracker: Tracker, private val start: Boolean, ) : ScreenModel { @@ -522,15 +534,15 @@ private data class TrackDateSelectorScreen( val localMillis = millis.convertEpochMillisZone(ZoneOffset.UTC, ZoneOffset.systemDefault()) coroutineScope.launchNonCancellable { if (start) { - service.setRemoteStartDate(track.toDbTrack(), localMillis) + tracker.setRemoteStartDate(track.toDbTrack(), localMillis) } else { - service.setRemoteFinishDate(track.toDbTrack(), localMillis) + tracker.setRemoteFinishDate(track.toDbTrack(), localMillis) } } } fun confirmRemoveDate(navigator: Navigator) { - navigator.push(TrackDateRemoverScreen(track, service.id, start)) + navigator.push(TrackDateRemoverScreen(track, tracker.id, start)) } } } @@ -547,7 +559,7 @@ private data class TrackDateRemoverScreen( val sm = rememberScreenModel { Model( track = track, - service = Injekt.get().getService(serviceId)!!, + tracker = Injekt.get().get(serviceId)!!, start = start, ) } @@ -584,7 +596,10 @@ private data class TrackDateRemoverScreen( Text(text = stringResource(android.R.string.cancel)) } FilledTonalButton( - onClick = { sm.removeDate(); navigator.popUntil { it is TrackInfoDialogHomeScreen } }, + onClick = { + sm.removeDate() + navigator.popUntil { it is TrackInfoDialogHomeScreen } + }, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer, @@ -599,25 +614,25 @@ private data class TrackDateRemoverScreen( private class Model( private val track: Track, - private val service: TrackService, + private val tracker: Tracker, private val start: Boolean, ) : ScreenModel { - fun getServiceName() = service.name + fun getServiceName() = tracker.name fun removeDate() { coroutineScope.launchNonCancellable { if (start) { - service.setRemoteStartDate(track.toDbTrack(), 0) + tracker.setRemoteStartDate(track.toDbTrack(), 0) } else { - service.setRemoteFinishDate(track.toDbTrack(), 0) + tracker.setRemoteFinishDate(track.toDbTrack(), 0) } } } } } -data class TrackServiceSearchScreen( +data class TrackerSearchScreen( private val mangaId: Long, private val initialQuery: String, private val currentUrl: String?, @@ -632,21 +647,24 @@ data class TrackServiceSearchScreen( mangaId = mangaId, currentUrl = currentUrl, initialQuery = initialQuery, - service = Injekt.get().getService(serviceId)!!, + tracker = Injekt.get().get(serviceId)!!, ) } val state by sm.state.collectAsState() var textFieldValue by remember { mutableStateOf(TextFieldValue(initialQuery)) } - TrackServiceSearch( + TrackerSearch( query = textFieldValue, onQueryChange = { textFieldValue = it }, onDispatchQuery = { sm.trackingSearch(textFieldValue.text) }, queryResult = state.queryResult, selected = state.selected, onSelectedChange = sm::updateSelection, - onConfirmSelection = { sm.registerTracking(state.selected!!); navigator.pop() }, + onConfirmSelection = { + sm.registerTracking(state.selected!!) + navigator.pop() + }, onDismissRequest = navigator::pop, ) } @@ -655,7 +673,7 @@ data class TrackServiceSearchScreen( private val mangaId: Long, private val currentUrl: String? = null, initialQuery: String, - private val service: TrackService, + private val tracker: Tracker, ) : StateScreenModel(State()) { init { @@ -672,7 +690,7 @@ data class TrackServiceSearchScreen( val result = withIOContext { try { - val results = service.search(query) + val results = tracker.search(query) Result.success(results) } catch (e: Throwable) { Result.failure(e) @@ -688,7 +706,7 @@ data class TrackServiceSearchScreen( } fun registerTracking(item: TrackSearch) { - coroutineScope.launchNonCancellable { service.register(item, mangaId) } + coroutineScope.launchNonCancellable { tracker.register(item, mangaId) } } fun updateSelection(selected: TrackSearch) { @@ -703,7 +721,7 @@ data class TrackServiceSearchScreen( } } -private data class TrackServiceRemoveScreen( +private data class TrackerRemoveScreen( private val mangaId: Long, private val track: Track, private val serviceId: Long, @@ -716,10 +734,10 @@ private data class TrackServiceRemoveScreen( Model( mangaId = mangaId, track = track, - service = Injekt.get().getService(serviceId)!!, + tracker = Injekt.get().get(serviceId)!!, ) } - val serviceName = sm.getServiceName() + val serviceName = sm.getName() var removeRemoteTrack by remember { mutableStateOf(false) } AlertDialogContent( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars), @@ -740,7 +758,7 @@ private data class TrackServiceRemoveScreen( Text( text = stringResource(R.string.track_delete_text, serviceName), ) - if (sm.isServiceDeletable()) { + if (sm.isDeletable()) { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox(checked = removeRemoteTrack, onCheckedChange = { removeRemoteTrack = it }) Text(text = stringResource(R.string.track_delete_remote_text, serviceName)) @@ -780,17 +798,17 @@ private data class TrackServiceRemoveScreen( private class Model( private val mangaId: Long, private val track: Track, - private val service: TrackService, + private val tracker: Tracker, private val deleteTrack: DeleteTrack = Injekt.get(), ) : ScreenModel { - fun getServiceName() = service.name + fun getName() = tracker.name - fun isServiceDeletable() = service is DeletableTrackService + fun isDeletable() = tracker is DeletableTracker fun deleteMangaFromService() { coroutineScope.launchNonCancellable { - (service as DeletableTrackService).delete(track.toDbTrack()) + (tracker as DeletableTracker).delete(track.toDbTrack()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt index d0883e593..fd46e0eec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.ui.manga.track -import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.Tracker import tachiyomi.domain.track.model.Track -data class TrackItem(val track: Track?, val service: TrackService) +data class TrackItem(val track: Track?, val tracker: Tracker) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 8d16c7b80..55be5fd61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -27,7 +27,6 @@ import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.ui.reader.model.StencilPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences @@ -405,8 +404,8 @@ class ReaderViewModel @JvmOverloads constructor( * [page]'s chapter is different from the currently active. */ fun onPageSelected(page: ReaderPage) { - // InsertPage and StencilPage doesn't change page progress - if (page is InsertPage || page is StencilPage) { + // InsertPage doesn't change page progress + if (page is InsertPage) { return } @@ -741,17 +740,14 @@ class ReaderViewModel @JvmOverloads constructor( val filename = generateFilename(manga, page) - // Pictures directory. - val relativePath = if (readerPreferences.folderPerManga().get()) DiskUtil.buildValidFilename(manga.title) else "" - - // Copy file in background. + // Copy file in background viewModelScope.launchNonCancellable { try { val uri = imageSaver.save( image = Image.Page( inputStream = page.stream!!, name = filename, - location = Location.Pictures.create(relativePath), + location = Location.Pictures(DiskUtil.buildValidFilename(manga.title)), ), ) withUIContext { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index 38ec9361d..4802708a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.suspendCancellableCoroutine -import tachiyomi.core.util.lang.awaitSingle import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.withIOContext import uy.kohesive.injekt.Injekt @@ -170,7 +169,7 @@ internal class HttpPageLoader( try { if (page.imageUrl.isNullOrEmpty()) { page.status = Page.State.LOAD_PAGE - page.imageUrl = source.fetchImageUrl(page).awaitSingle() + page.imageUrl = source.getImageUrl(page) } val imageUrl = page.imageUrl!! diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/StencilPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/StencilPage.kt deleted file mode 100644 index c5a7ee79f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/StencilPage.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.model - -import java.io.InputStream - -class StencilPage( - parent: ReaderPage, - stencilStream: () -> InputStream, -) : ReaderPage(parent.index, parent.url, parent.imageUrl) { - - override var chapter: ReaderChapter = parent.chapter - - init { - status = State.READY - stream = stencilStream - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt index 23a5beb13..faf75d0ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.setting import androidx.annotation.StringRes import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.isReleaseBuildType import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.getEnum @@ -33,9 +32,6 @@ class ReaderPreferences( fun defaultOrientationType() = preferenceStore.getInt("pref_default_orientation_type_key", OrientationType.FREE.flagValue) - // TODO: Enable in release build when the feature is stable - fun longStripSplitWebtoon() = preferenceStore.getBoolean("pref_long_strip_split_webtoon", !isReleaseBuildType) - fun webtoonDoubleTapZoomEnabled() = preferenceStore.getBoolean("pref_enable_double_tap_zoom_webtoon", true) fun imageScaleType() = preferenceStore.getInt("pref_image_scale_type_key", 1) @@ -58,8 +54,6 @@ class ReaderPreferences( fun readerHideThreshold() = preferenceStore.getEnum("reader_hide_threshold", ReaderHideThreshold.LOW) - fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false) - fun skipRead() = preferenceStore.getBoolean("skip_read", false) fun skipFiltered() = preferenceStore.getBoolean("skip_filtered", true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt index 87edd1da4..036f767f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt @@ -7,12 +7,10 @@ import androidx.recyclerview.widget.RecyclerView import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.ui.reader.model.StencilPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.calculateChapterGap import eu.kanade.tachiyomi.util.system.createReaderThemeContext -import tachiyomi.core.util.system.logcat /** * RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted. @@ -27,27 +25,6 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter) { - if (newStrips.isEmpty()) return - if (currentStrip is StencilPage) return - - val placeAtIndex = items.indexOf(currentStrip) + 1 - // Stop constantly adding split images - if (items.getOrNull(placeAtIndex) is StencilPage) return - - val updatedItems = items.toMutableList() - updatedItems.addAll(placeAtIndex, newStrips) - updateItems(updatedItems) - logcat { "New adapter item count is $itemCount" } - } - - fun cleanupSplitStrips() { - if (items.any { it is StencilPage }) { - val updatedItems = items.filterNot { it is StencilPage } - updateItems(updatedItems) - } - } - /** * Context that has been wrapped to use the correct theme values based on the * current app theme and reader background color diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt index b2e0045b8..a64826ab1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt @@ -32,11 +32,6 @@ class WebtoonConfig( var sidePadding = 0 private set - var longStripSplit = false - private set - - var longStripSplitChangedListener: ((Boolean) -> Unit)? = null - var doubleTapZoom = true private set @@ -67,15 +62,6 @@ class WebtoonConfig( readerPreferences.dualPageInvertWebtoon() .register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() }) - readerPreferences.longStripSplitWebtoon() - .register( - { longStripSplit = it }, - { - imagePropertyChangedListener?.invoke() - longStripSplitChangedListener?.invoke(it) - }, - ) - readerPreferences.webtoonDoubleTapZoomEnabled() .register( { doubleTapZoom = it }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 749a48e33..7fa2058d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -13,7 +13,6 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import eu.kanade.tachiyomi.databinding.ReaderErrorBinding import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.ui.reader.model.StencilPage import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.webview.WebViewActivity @@ -24,12 +23,10 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.suspendCancellableCoroutine -import logcat.LogPriority import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.ImageUtil -import tachiyomi.core.util.system.logcat import java.io.BufferedInputStream import java.io.InputStream @@ -221,40 +218,9 @@ class WebtoonPageHolder( } } - if (viewer.config.longStripSplit) { - if (page is StencilPage) { - return imageStream - } - val isStripSplitNeeded = ImageUtil.isStripSplitNeeded(imageStream) - if (isStripSplitNeeded) { - return onStripSplit(imageStream) - } - } - return imageStream } - private fun onStripSplit(imageStream: BufferedInputStream): InputStream { - try { - // If we have reached this point [page] and its stream shouldn't be null - val page = page!! - val stream = page.stream!! - val splitData = ImageUtil.getSplitDataForStream(imageStream).toMutableList() - val currentSplitData = splitData.removeFirst() - val newPages = splitData.map { - StencilPage(page) { ImageUtil.splitStrip(it, stream) } - } - return ImageUtil.splitStrip(currentSplitData) { imageStream } - .also { - // Running [onLongStripSplit] first results in issues with splitting - viewer.onLongStripSplit(page, newPages) - } - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to split image" } - return imageStream - } - } - /** * Called when the page has an error. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index d7a0290f3..d85a5b04b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderPage -import eu.kanade.tachiyomi.ui.reader.model.StencilPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.viewer.Viewer @@ -151,12 +150,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart) } - config.longStripSplitChangedListener = { enabled -> - if (!enabled) { - cleanupSplitStrips() - } - } - frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) frame.addView(recycler) } @@ -205,11 +198,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr logcat { "onPageSelected: ${page.number}/${pages.size}" } activity.onPageSelected(page) - // Skip preload on StencilPage - if (page is StencilPage) { - return - } - // Preload next chapter once we're within the last 5 pages of the current chapter val inPreloadRange = pages.size - page.number < 5 if (inPreloadRange && allowPreload && page.chapter == adapter.currentChapter) { @@ -359,15 +347,4 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr min(position + 3, adapter.itemCount - 1), ) } - - fun onLongStripSplit(currentStrip: Any?, newStrips: List) { - activity.runOnUiThread { - // Need to insert on UI thread else images will go blank - adapter.onLongStripSplit(currentStrip, newStrips) - } - } - - private fun cleanupSplitStrips() { - adapter.cleanupSplitStrips() - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt index b697a98d1..09ec00169 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt @@ -19,7 +19,7 @@ class UnlockActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) startAuthentication( - getString(R.string.unlock_app), + getString(R.string.unlock_app_title, getString(R.string.app_name)), confirmationRequired = false, callback = object : AuthenticatorUtil.AuthenticationCallback() { override fun onAuthenticationError( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt index a1a91a003..92302d718 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/BaseOAuthLoginActivity.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.setting.track import android.content.Intent import android.net.Uri import android.os.Bundle -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.view.setComposeContent @@ -12,7 +12,7 @@ import uy.kohesive.injekt.injectLazy abstract class BaseOAuthLoginActivity : BaseActivity() { - internal val trackManager: TrackManager by injectLazy() + internal val trackerManager: TrackerManager by injectLazy() abstract fun handleResult(data: Uri?) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt index af13de51b..47cd88416 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt @@ -20,11 +20,11 @@ class TrackLoginActivity : BaseOAuthLoginActivity() { val matchResult = regex.find(data.fragment.toString()) if (matchResult?.groups?.get(1) != null) { lifecycleScope.launchIO { - trackManager.aniList.login(matchResult.groups[1]!!.value) + trackerManager.aniList.login(matchResult.groups[1]!!.value) returnToSettings() } } else { - trackManager.aniList.logout() + trackerManager.aniList.logout() returnToSettings() } } @@ -33,11 +33,11 @@ class TrackLoginActivity : BaseOAuthLoginActivity() { val code = data.getQueryParameter("code") if (code != null) { lifecycleScope.launchIO { - trackManager.bangumi.login(code) + trackerManager.bangumi.login(code) returnToSettings() } } else { - trackManager.bangumi.logout() + trackerManager.bangumi.logout() returnToSettings() } } @@ -46,11 +46,11 @@ class TrackLoginActivity : BaseOAuthLoginActivity() { val code = data.getQueryParameter("code") if (code != null) { lifecycleScope.launchIO { - trackManager.myAnimeList.login(code) + trackerManager.myAnimeList.login(code) returnToSettings() } } else { - trackManager.myAnimeList.logout() + trackerManager.myAnimeList.logout() returnToSettings() } } @@ -59,11 +59,11 @@ class TrackLoginActivity : BaseOAuthLoginActivity() { val code = data.getQueryParameter("code") if (code != null) { lifecycleScope.launchIO { - trackManager.shikimori.login(code) + trackerManager.shikimori.login(code) returnToSettings() } } else { - trackManager.shikimori.logout() + trackerManager.shikimori.logout() returnToSettings() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt index 873bd2723..6131308d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt @@ -10,7 +10,7 @@ import eu.kanade.core.util.fastMapNotNull import eu.kanade.presentation.more.stats.StatsScreenState import eu.kanade.presentation.more.stats.data.StatsData import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.source.model.SManga import kotlinx.coroutines.flow.update import tachiyomi.core.util.lang.launchIO @@ -33,10 +33,10 @@ class StatsScreenModel( private val getTotalReadDuration: GetTotalReadDuration = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), private val preferences: LibraryPreferences = Injekt.get(), - private val trackManager: TrackManager = Injekt.get(), + private val trackerManager: TrackerManager = Injekt.get(), ) : StateScreenModel(StatsScreenState.Loading) { - private val loggedServices by lazy { trackManager.services.fastFilter { it.isLoggedIn } } + private val loggedInTrackers by lazy { trackerManager.trackers.fastFilter { it.isLoggedIn } } init { coroutineScope.launchIO { @@ -72,7 +72,7 @@ class StatsScreenModel( val trackersStatData = StatsData.Trackers( trackedTitleCount = mangaTrackMap.count { it.value.isNotEmpty() }, meanScore = meanScore, - trackerCount = loggedServices.size, + trackerCount = loggedInTrackers.size, ) mutableState.update { @@ -115,10 +115,10 @@ class StatsScreenModel( } private suspend fun getMangaTrackMap(libraryManga: List): Map> { - val loggedServicesIds = loggedServices.map { it.id }.toHashSet() + val loggedInTrackerIds = loggedInTrackers.map { it.id }.toHashSet() return libraryManga.associate { manga -> val tracks = getTracks.await(manga.id) - .fastFilter { it.syncId in loggedServicesIds } + .fastFilter { it.syncId in loggedInTrackerIds } manga.id to tracks } @@ -144,7 +144,7 @@ class StatsScreenModel( } private fun get10PointScore(track: Track): Double { - val service = trackManager.getService(track.syncId)!! + val service = trackerManager.get(track.syncId)!! return service.get10PointScore(track) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 6459d6525..62ac21cc1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -59,12 +59,14 @@ class UpdatesScreenModel( private val getChapter: GetChapter = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), + uiPreferences: UiPreferences = Injekt.get(), ) : StateScreenModel(State()) { private val _events: Channel = Channel(Int.MAX_VALUE) val events: Flow = _events.receiveAsFlow() val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope) + val relativeTime by uiPreferences.relativeTime().asState(coroutineScope) // First and last selected index in list private val selectedPositions: Array = arrayOf(-1, -1) @@ -374,7 +376,7 @@ class UpdatesScreenModel( val selected = items.filter { it.selected } val selectionMode = selected.isNotEmpty() - fun getUiModel(context: Context): List { + fun getUiModel(context: Context, relativeTime: Boolean): List { val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get().dateFormat().get())) return items @@ -384,7 +386,11 @@ class UpdatesScreenModel( val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0) when { beforeDate.time != afterDate.time && afterDate.time != 0L -> { - val text = afterDate.toRelativeString(context, dateFormat) + val text = afterDate.toRelativeString( + context = context, + relative = relativeTime, + dateFormat = dateFormat, + ) UpdatesUiModel.Header(text) } // Return null to avoid adding a separator between two items. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index cf33f69c4..78a44dd23 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -57,6 +57,7 @@ object UpdatesTab : Tab { state = state, snackbarHostState = screenModel.snackbarHostState, lastUpdated = screenModel.lastUpdated, + relativeTime = screenModel.relativeTime, onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) }, onSelectAll = screenModel::toggleAllSelection, onInvertSelection = screenModel::invertSelection, diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt index 0abd22f13..e67d7cd2f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util import android.content.Context import android.net.Uri -import eu.kanade.tachiyomi.data.backup.BackupManager +import eu.kanade.tachiyomi.data.backup.BackupCreator import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupSerializer import okio.buffer @@ -14,7 +14,7 @@ object BackupUtil { * Decode a potentially-gzipped backup. */ fun decodeBackup(context: Context, uri: Uri): Backup { - val backupManager = BackupManager(context) + val backupCreator = BackupCreator(context) val backupStringSource = context.contentResolver.openInputStream(uri)!!.source().buffer() @@ -27,6 +27,6 @@ object BackupUtil { backupStringSource }.use { it.readByteArray() } - return backupManager.parser.decodeFromByteArray(BackupSerializer, backupString) + return backupCreator.parser.decodeFromByteArray(BackupSerializer, backupString) } } 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 aa9830de8..c495c7008 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Build import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.util.storage.getUriCompat +import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast @@ -35,6 +36,7 @@ class CrashLogUtil(private val context: Context) { Device name: ${Build.DEVICE} Device model: ${Build.MODEL} Device product name: ${Build.PRODUCT} + WebView user agent: ${WebViewUtil.getInferredUserAgent(context)} """.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 9efcaaa9f..e2f683685 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 @@ -8,7 +8,6 @@ import java.time.LocalDateTime import java.time.ZoneId import java.util.Calendar import java.util.Date -import java.util.TimeZone fun Date.toDateTimestampString(dateFormatter: DateFormat): String { val date = dateFormatter.format(this) @@ -46,76 +45,16 @@ fun Long.toDateKey(): Date { return cal.time } -/** - * Convert epoch long to Calendar instance - * - * @return Calendar instance at supplied epoch time. Null if epoch was 0. - */ -fun Long.toCalendar(): Calendar? { - if (this == 0L) { - return null - } - val cal = Calendar.getInstance() - cal.timeInMillis = this - return cal -} - -/** - * Convert local time millisecond value to Calendar instance in UTC - * - * @return UTC Calendar instance at supplied time. Null if time is 0. - */ -fun Long.toUtcCalendar(): Calendar? { - if (this == 0L) { - return null - } - val rawCalendar = Calendar.getInstance().apply { - timeInMillis = this@toUtcCalendar - } - return Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { - clear() - set( - rawCalendar.get(Calendar.YEAR), - rawCalendar.get(Calendar.MONTH), - rawCalendar.get(Calendar.DAY_OF_MONTH), - rawCalendar.get(Calendar.HOUR_OF_DAY), - rawCalendar.get(Calendar.MINUTE), - rawCalendar.get(Calendar.SECOND), - ) - } -} - -/** - * Convert UTC time millisecond to Calendar instance in local time zone - * - * @return local Calendar instance at supplied UTC time. Null if time is 0. - */ -fun Long.toLocalCalendar(): Calendar? { - if (this == 0L) { - return null - } - val rawCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { - timeInMillis = this@toLocalCalendar - } - return Calendar.getInstance().apply { - clear() - set( - rawCalendar.get(Calendar.YEAR), - rawCalendar.get(Calendar.MONTH), - rawCalendar.get(Calendar.DAY_OF_MONTH), - rawCalendar.get(Calendar.HOUR_OF_DAY), - rawCalendar.get(Calendar.MINUTE), - rawCalendar.get(Calendar.SECOND), - ) - } -} - private const val MILLISECONDS_IN_DAY = 86_400_000L fun Date.toRelativeString( context: Context, + relative: Boolean = true, dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT), ): String { + if (!relative) { + return dateFormat.format(this) + } val now = Date() val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt() diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt index 50f45ae85..3e2834fd2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt @@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.util.system import android.app.Activity import android.content.Context import android.content.res.Configuration -import android.content.res.Resources import android.os.Build -import android.view.View import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.model.TabletUiMode import uy.kohesive.injekt.Injekt @@ -64,18 +62,6 @@ fun Context.isNightMode(): Boolean { return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES } -val Resources.isLTR - get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR - -/** - * Converts to px and takes into account LTR/RTL layout. - */ -val Float.dpToPxEnd: Float - get() = ( - this * Resources.getSystem().displayMetrics.density * - if (Resources.getSystem().isLTR) 1 else -1 - ) - /** * Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.). * diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialFastScroll.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialFastScroll.kt deleted file mode 100644 index 29e9e43e3..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/MaterialFastScroll.kt +++ /dev/null @@ -1,92 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import androidx.core.view.ViewCompat -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.fastscroller.FastScroller -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.dpToPxEnd -import eu.kanade.tachiyomi.util.system.isLTR - -class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - FastScroller(context, attrs) { - - init { - setViewsToUse( - R.layout.material_fastscroll, - R.id.fast_scroller_bubble, - R.id.fast_scroller_handle, - ) - autoHideEnabled = true - ignoreTouchesOutsideHandle = true - - applyInsetter { - type(navigationBars = true) { - margin() - } - } - } - - // Overridden to handle RTL - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent): Boolean { - if (recyclerView.computeVerticalScrollRange() <= recyclerView.computeVerticalScrollExtent()) { - return super.onTouchEvent(event) - } - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - // start: handle RTL differently - if ( - if (context.resources.isLTR) { - event.x < handle.x - ViewCompat.getPaddingStart(handle) - } else { - event.x > handle.width + ViewCompat.getPaddingStart(handle) - } - ) { - return false - } - // end - - if (ignoreTouchesOutsideHandle && - (event.y < handle.y || event.y > handle.y + handle.height) - ) { - return false - } - handle.isSelected = true - notifyScrollStateChange(true) - showBubble() - showScrollbar() - val y = event.y - setBubbleAndHandlePosition(y) - setRecyclerViewPosition(y) - return true - } - MotionEvent.ACTION_MOVE -> { - val y = event.y - setBubbleAndHandlePosition(y) - setRecyclerViewPosition(y) - return true - } - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - handle.isSelected = false - notifyScrollStateChange(false) - hideBubble() - if (autoHideEnabled) hideScrollbar() - return true - } - } - return super.onTouchEvent(event) - } - - override fun setBubbleAndHandlePosition(y: Float) { - super.setBubbleAndHandlePosition(y) - if (bubbleEnabled) { - bubble.y = handle.y - bubble.height / 2f + handle.height / 2f - bubble.translationX = (-45f).dpToPxEnd - } - } -} diff --git a/app/src/main/res/drawable/material_thumb_drawable.xml b/app/src/main/res/drawable/material_thumb_drawable.xml deleted file mode 100644 index 2e550c0df..000000000 --- a/app/src/main/res/drawable/material_thumb_drawable.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/download_list.xml b/app/src/main/res/layout/download_list.xml index e2c96ff12..ec3e3bab9 100644 --- a/app/src/main/res/layout/download_list.xml +++ b/app/src/main/res/layout/download_list.xml @@ -1,24 +1,7 @@ - - - - - - - + android:layout_height="match_parent" + android:clipToPadding="false" + tools:listitem="@layout/download_item" /> diff --git a/app/src/main/res/layout/material_fastscroll.xml b/app/src/main/res/layout/material_fastscroll.xml deleted file mode 100644 index 56b16a1b8..000000000 --- a/app/src/main/res/layout/material_fastscroll.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/download_single.xml b/app/src/main/res/menu/download_single.xml index 34ec33a4d..1cf14ff0c 100644 --- a/app/src/main/res/menu/download_single.xml +++ b/app/src/main/res/menu/download_single.xml @@ -13,6 +13,10 @@ android:id="@+id/move_to_bottom" android:title="@string/action_move_to_bottom" /> + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 14dc1a13a..22313ff42 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -56,21 +56,6 @@ - - - - - diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index b5854fcb5..2c76035b7 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -5,7 +5,7 @@ plugins { dependencies { implementation(androidxLibs.gradle) implementation(kotlinLibs.gradle) - implementation(libs.kotlinter) + implementation(libs.ktlint) implementation(gradleApi()) } diff --git a/buildSrc/src/main/kotlin/tachiyomi.lint.gradle.kts b/buildSrc/src/main/kotlin/tachiyomi.lint.gradle.kts index 12b7b2295..a89d88592 100644 --- a/buildSrc/src/main/kotlin/tachiyomi.lint.gradle.kts +++ b/buildSrc/src/main/kotlin/tachiyomi.lint.gradle.kts @@ -1,20 +1,14 @@ -import org.jmailen.gradle.kotlinter.KotlinterExtension -import org.jmailen.gradle.kotlinter.KotlinterPlugin +import org.jlleitschuh.gradle.ktlint.KtlintExtension +import org.jlleitschuh.gradle.ktlint.KtlintPlugin -apply() +apply() -extensions.configure("kotlinter") { - experimentalRules = true +extensions.configure("ktlint") { + version.set("0.50.0") + android.set(true) + enableExperimentalRules.set(true) - disabledRules = arrayOf( - "experimental:argument-list-wrapping", // Doesn't play well with Android Studio - "filename", // Often broken to give a more general name - ) -} - -tasks { - named("preBuild").configure { - if (!System.getenv("CI").toBoolean()) - dependsOn("formatKotlin") + filter { + exclude("**/generated/**") } } diff --git a/core-metadata/build.gradle.kts b/core-metadata/build.gradle.kts index 51db46b91..43b8846ab 100644 --- a/core-metadata/build.gradle.kts +++ b/core-metadata/build.gradle.kts @@ -11,7 +11,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } - } dependencies { diff --git a/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt index 06cce7ccc..681b4819b 100644 --- a/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt +++ b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt @@ -8,6 +8,27 @@ import nl.adaptivity.xmlutil.serialization.XmlValue const val COMIC_INFO_FILE = "ComicInfo.xml" +fun SManga.getComicInfo() = ComicInfo( + series = ComicInfo.Series(title), + summary = description?.let { ComicInfo.Summary(it) }, + writer = author?.let { ComicInfo.Writer(it) }, + penciller = artist?.let { ComicInfo.Penciller(it) }, + genre = genre?.let { ComicInfo.Genre(it) }, + publishingStatus = ComicInfo.PublishingStatusTachiyomi( + ComicInfoPublishingStatus.toComicInfoValue(status.toLong()), + ), + title = null, + number = null, + web = null, + translator = null, + inker = null, + colorist = null, + letterer = null, + coverArtist = null, + tags = null, + categories = null, +) + fun SManga.copyFromComicInfo(comicInfo: ComicInfo) { comicInfo.series?.let { title = it.value } comicInfo.writer?.let { author = it.value } @@ -39,6 +60,8 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) { status = ComicInfoPublishingStatus.toSMangaValue(comicInfo.publishingStatus?.value) } +// https://anansi-project.github.io/docs/comicinfo/schemas/v2.0 +@Suppress("UNUSED") @Serializable @XmlSerialName("ComicInfo", "", "") data class ComicInfo( @@ -59,12 +82,10 @@ data class ComicInfo( val publishingStatus: PublishingStatusTachiyomi?, val categories: CategoriesTachiyomi?, ) { - @Suppress("UNUSED") @XmlElement(false) @XmlSerialName("xmlns:xsd", "", "") val xmlSchema: String = "http://www.w3.org/2001/XMLSchema" - @Suppress("UNUSED") @XmlElement(false) @XmlSerialName("xmlns:xsi", "", "") val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance" diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 38ba749d2..6893a0495 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -39,7 +39,9 @@ class NetworkHelper( builder.addNetworkInterceptor(httpLoggingInterceptor) } - builder.addInterceptor(CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider)) + builder.addInterceptor( + CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider), + ) when (preferences.dohProvider().get()) { PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt index 5e14d0342..cba50e99c 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -17,6 +17,9 @@ class NetworkPreferences( } fun defaultUserAgent(): Preference { - return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0") + return preferenceStore.getString( + "default_user_agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0", + ) } } diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/core/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index 4cbb34812..e6eec02f4 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -58,6 +58,15 @@ fun Call.asObservable(): Observable { } } +fun Call.asObservableSuccess(): Observable { + return asObservable().doOnNext { response -> + if (!response.isSuccessful) { + response.close() + throw HttpException(response.code) + } + } +} + // Based on https://github.com/gildor/kotlin-coroutines-okhttp @OptIn(ExperimentalCoroutinesApi::class) private suspend fun Call.await(callStack: Array): Response { @@ -95,6 +104,9 @@ suspend fun Call.await(): Response { return await(callStack) } +/** + * @since extensions-lib 1.5 + */ suspend fun Call.awaitSuccess(): Response { val callStack = Exception().stackTrace.run { copyOfRange(1, size) } val response = await(callStack) @@ -105,15 +117,6 @@ suspend fun Call.awaitSuccess(): Response { return response } -fun Call.asObservableSuccess(): Observable { - return asObservable().doOnNext { response -> - if (!response.isSuccessful) { - response.close() - throw HttpException(response.code) - } - } -} - fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call { val progressClient = newBuilder() .cache(null) diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt b/core/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt index 72248f17b..6ba53b197 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt @@ -9,7 +9,10 @@ import okio.Source import okio.buffer import java.io.IOException -class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { +class ProgressResponseBody( + private val responseBody: ResponseBody, + private val progressListener: ProgressListener, +) : ResponseBody() { private val bufferedSource: BufferedSource by lazy { source(responseBody.source()).buffer() @@ -36,7 +39,11 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p val bytesRead = super.read(sink, byteCount) // read() returns the number of bytes read, or -1 if this source is exhausted. totalBytesRead += if (bytesRead != -1L) bytesRead else 0 - progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + progressListener.update( + totalBytesRead, + responseBody.contentLength(), + bytesRead == -1L, + ) return bytesRead } } diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 25b5a140a..d5893bb05 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -31,7 +31,11 @@ class CloudflareInterceptor( return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK } - override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response { + override fun intercept( + chain: Interceptor.Chain, + request: Request, + response: Response, + ): Response { try { response.close() cookieManager.remove(request.url, COOKIE_NAMES, 0) diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt index b364db743..c86f780d9 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt @@ -33,7 +33,9 @@ fun OkHttpClient.Builder.rateLimitHost( permits: Int, period: Long = 1, unit: TimeUnit = TimeUnit.SECONDS, -) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit()))) +) = addInterceptor( + RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())), +) /** * An OkHttp interceptor that handles given url host's rate limiting. @@ -69,8 +71,5 @@ fun OkHttpClient.Builder.rateLimitHost( * @param permits [Int] Number of requests allowed within a period of units. * @param period [Duration] The limiting duration. Defaults to 1.seconds. */ -fun OkHttpClient.Builder.rateLimitHost( - url: String, - permits: Int, - period: Duration = 1.seconds, -) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period)) +fun OkHttpClient.Builder.rateLimitHost(url: String, permits: Int, period: Duration = 1.seconds) = + addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period)) diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/system/ToastExtensions.kt b/core/src/main/java/eu/kanade/tachiyomi/util/system/ToastExtensions.kt index 4901e7463..9e4554cb4 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/util/system/ToastExtensions.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/system/ToastExtensions.kt @@ -10,7 +10,11 @@ import androidx.annotation.StringRes * @param resource the text resource. * @param duration the duration of the toast. Defaults to short. */ -fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast { +fun Context.toast( + @StringRes resource: Int, + duration: Int = Toast.LENGTH_SHORT, + block: (Toast) -> Unit = {}, +): Toast { return toast(getString(resource), duration, block) } @@ -20,7 +24,11 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT, * @param text the text to display. * @param duration the duration of the toast. Defaults to short. */ -fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast { +fun Context.toast( + text: String?, + duration: Int = Toast.LENGTH_SHORT, + block: (Toast) -> Unit = {}, +): Toast { return Toast.makeText(applicationContext, text.orEmpty(), duration).also { block(it) it.show() diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt b/core/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt index 6c6fba4ff..93ce0337d 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt @@ -47,10 +47,7 @@ abstract class WebViewClientCompat : WebViewClient() { return shouldInterceptRequestCompat(view, request.url.toString()) } - final override fun shouldInterceptRequest( - view: WebView, - url: String, - ): WebResourceResponse? { + final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { return shouldInterceptRequestCompat(view, url) } diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/core/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt index 4fcaaceed..eb1ad9617 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt @@ -14,7 +14,24 @@ import kotlin.coroutines.resume object WebViewUtil { const val SPOOF_PACKAGE_NAME = "org.chromium.chrome" - const val MINIMUM_WEBVIEW_VERSION = 111 + const val MINIMUM_WEBVIEW_VERSION = 114 + + /** + * Uses the WebView's user agent string to create something similar to what Chrome on Android + * would return. + * + * Example of WebView user agent string: + * Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.0.0 Mobile Safari/537.36 + * + * Example of Chrome on Android: + * Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.3 + */ + fun getInferredUserAgent(context: Context): String { + return WebView(context) + .getDefaultUserAgentString() + .replace("; Android .*?\\)".toRegex(), "; Android 10; K)") + .replace("Version/.* Chrome/".toRegex(), "Chrome/") + } fun supportsWebView(context: Context): Boolean { try { diff --git a/core/src/main/java/tachiyomi/core/Constants.kt b/core/src/main/java/tachiyomi/core/Constants.kt index 156ed6101..c5631535f 100644 --- a/core/src/main/java/tachiyomi/core/Constants.kt +++ b/core/src/main/java/tachiyomi/core/Constants.kt @@ -1,7 +1,7 @@ package tachiyomi.core object Constants { - const val URL_HELP = "https://tachiyomi.org/help/" + const val URL_HELP = "https://tachiyomi.org/docs/guides/troubleshooting/" const val MANGA_EXTRA = "manga" diff --git a/core/src/main/java/tachiyomi/core/preference/AndroidPreference.kt b/core/src/main/java/tachiyomi/core/preference/AndroidPreference.kt index 00f3701d0..21f5b1d30 100644 --- a/core/src/main/java/tachiyomi/core/preference/AndroidPreference.kt +++ b/core/src/main/java/tachiyomi/core/preference/AndroidPreference.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import tachiyomi.core.util.system.logcat sealed class AndroidPreference( private val preferences: SharedPreferences, @@ -29,7 +30,13 @@ sealed class AndroidPreference( } override fun get(): T { - return read(preferences, key, defaultValue) + return try { + read(preferences, key, defaultValue) + } catch (e: ClassCastException) { + logcat { "Invalid value for $key; deleting" } + delete() + defaultValue + } } override fun set(value: T) { @@ -68,7 +75,11 @@ sealed class AndroidPreference( key: String, defaultValue: String, ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { - override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String { + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: String, + ): String { return preferences.getString(key, defaultValue) ?: defaultValue } @@ -128,7 +139,11 @@ sealed class AndroidPreference( key: String, defaultValue: Boolean, ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { - override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean { + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: Boolean, + ): Boolean { return preferences.getBoolean(key, defaultValue) } @@ -143,7 +158,11 @@ sealed class AndroidPreference( key: String, defaultValue: Set, ) : AndroidPreference>(preferences, keyFlow, key, defaultValue) { - override fun read(preferences: SharedPreferences, key: String, defaultValue: Set): Set { + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: Set, + ): Set { return preferences.getStringSet(key, defaultValue) ?: defaultValue } diff --git a/core/src/main/java/tachiyomi/core/preference/AndroidPreferenceStore.kt b/core/src/main/java/tachiyomi/core/preference/AndroidPreferenceStore.kt index 74c2872d8..b24fa5dcc 100644 --- a/core/src/main/java/tachiyomi/core/preference/AndroidPreferenceStore.kt +++ b/core/src/main/java/tachiyomi/core/preference/AndroidPreferenceStore.kt @@ -15,10 +15,9 @@ import tachiyomi.core.preference.AndroidPreference.StringSetPrimitive class AndroidPreferenceStore( context: Context, + private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context), ) : PreferenceStore { - private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - private val keyFlow = sharedPreferences.keyFlow override fun getString(key: String, defaultValue: String): Preference { @@ -60,11 +59,19 @@ class AndroidPreferenceStore( deserializer = deserializer, ) } + + override fun getAll(): Map { + return sharedPreferences.all ?: emptyMap() + } } private val SharedPreferences.keyFlow get() = callbackFlow { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) } + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> + trySend( + key, + ) + } registerOnSharedPreferenceChangeListener(listener) awaitClose { unregisterOnSharedPreferenceChangeListener(listener) diff --git a/core/src/main/java/tachiyomi/core/preference/Preference.kt b/core/src/main/java/tachiyomi/core/preference/Preference.kt index 67b7a44f4..e10f6e7fe 100644 --- a/core/src/main/java/tachiyomi/core/preference/Preference.kt +++ b/core/src/main/java/tachiyomi/core/preference/Preference.kt @@ -21,9 +21,29 @@ interface Preference { fun changes(): Flow fun stateIn(scope: CoroutineScope): StateFlow + + val isPrivate: Boolean + get() = key().startsWith(PRIVATE_PREFIX) + + companion object { + /** + * A preference that should not be exposed in places like backups. + */ + fun isPrivate(key: String): Boolean { + return key.startsWith(PRIVATE_PREFIX) + } + + fun privateKey(key: String): String { + return "${PRIVATE_PREFIX}$key" + } + + private const val PRIVATE_PREFIX = "__PRIVATE_" + } } -inline fun Preference.getAndSet(crossinline block: (T) -> R) = set(block(get())) +inline fun Preference.getAndSet(crossinline block: (T) -> R) = set( + block(get()), +) operator fun Preference>.plusAssign(item: T) { set(get() + item) diff --git a/core/src/main/java/tachiyomi/core/preference/PreferenceStore.kt b/core/src/main/java/tachiyomi/core/preference/PreferenceStore.kt index 86158ad63..55ba9597f 100644 --- a/core/src/main/java/tachiyomi/core/preference/PreferenceStore.kt +++ b/core/src/main/java/tachiyomi/core/preference/PreferenceStore.kt @@ -31,6 +31,8 @@ interface PreferenceStore { serializer: (T) -> String, deserializer: (String) -> T, ): Preference + + fun getAll(): Map } inline fun > PreferenceStore.getEnum( diff --git a/core/src/main/java/tachiyomi/core/util/lang/CoroutinesExtensions.kt b/core/src/main/java/tachiyomi/core/util/lang/CoroutinesExtensions.kt index 4573b2d85..829207a9b 100644 --- a/core/src/main/java/tachiyomi/core/util/lang/CoroutinesExtensions.kt +++ b/core/src/main/java/tachiyomi/core/util/lang/CoroutinesExtensions.kt @@ -52,9 +52,15 @@ fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job = fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job = launchIO { withContext(NonCancellable, block) } -suspend fun withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block) +suspend fun withUIContext(block: suspend CoroutineScope.() -> T) = withContext( + Dispatchers.Main, + block, +) -suspend fun withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block) +suspend fun withIOContext(block: suspend CoroutineScope.() -> T) = withContext( + Dispatchers.IO, + block, +) suspend fun withNonCancellableContext(block: suspend CoroutineScope.() -> T) = withContext(NonCancellable, block) diff --git a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt index fdfff4603..62a0d347a 100644 --- a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt +++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt @@ -273,48 +273,6 @@ object ImageUtil { private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(index + 1)}.jpg" - /** - * Check whether the image is a long Strip that needs splitting - * @return true if the image is not animated and it's height is greater than image width and screen height - */ - fun isStripSplitNeeded(imageStream: BufferedInputStream): Boolean { - if (isAnimatedAndSupported(imageStream)) return false - - val options = extractImageOptions(imageStream) - val imageHeightIsBiggerThanWidth = options.outHeight > options.outWidth - val imageHeightBiggerThanScreenHeight = options.outHeight > optimalImageHeight - return imageHeightIsBiggerThanWidth && imageHeightBiggerThanScreenHeight - } - - /** - * Split the imageStream according to the provided splitData - */ - fun splitStrip(splitData: SplitData, streamFn: () -> InputStream): InputStream { - val bitmapRegionDecoder = getBitmapRegionDecoder(streamFn()) - ?: throw Exception("Failed to create new instance of BitmapRegionDecoder") - - logcat { - "WebtoonSplit #${splitData.index} with topOffset=${splitData.topOffset} " + - "splitHeight=${splitData.splitHeight} bottomOffset=${splitData.bottomOffset}" - } - - try { - val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset) - val splitBitmap = bitmapRegionDecoder.decodeRegion(region, null) - val outputStream = ByteArrayOutputStream() - splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) - return ByteArrayInputStream(outputStream.toByteArray()) - } catch (e: Throwable) { - throw e - } finally { - bitmapRegionDecoder.recycle() - } - } - - fun getSplitDataForStream(imageStream: InputStream): List { - return extractImageOptions(imageStream).splitData - } - private val BitmapFactory.Options.splitData get(): List { val imageHeight = outHeight diff --git a/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt b/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt index 4a3475b4f..596a31286 100644 --- a/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt +++ b/data/src/main/java/tachiyomi/data/DatabaseAdapter.kt @@ -11,13 +11,14 @@ object DateColumnAdapter : ColumnAdapter { private const val LIST_OF_STRINGS_SEPARATOR = ", " object StringListColumnAdapter : ColumnAdapter, String> { - override fun decode(databaseValue: String) = - if (databaseValue.isEmpty()) { - emptyList() - } else { - databaseValue.split(LIST_OF_STRINGS_SEPARATOR) - } - override fun encode(value: List) = value.joinToString(separator = LIST_OF_STRINGS_SEPARATOR) + override fun decode(databaseValue: String) = if (databaseValue.isEmpty()) { + emptyList() + } else { + databaseValue.split(LIST_OF_STRINGS_SEPARATOR) + } + override fun encode(value: List) = value.joinToString( + separator = LIST_OF_STRINGS_SEPARATOR, + ) } object UpdateStrategyColumnAdapter : ColumnAdapter { diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt index 71ae1720e..c8243682f 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt @@ -2,7 +2,21 @@ package tachiyomi.data.chapter import tachiyomi.domain.chapter.model.Chapter -val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Double, Long, Long, Long, Long) -> Chapter = +val chapterMapper: ( + Long, + Long, + String, + String, + String?, + Boolean, + Boolean, + Long, + Double, + Long, + Long, + Long, + Long, +) -> Chapter = { id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt -> Chapter( id = id, diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt index 45a61e363..594a592ac 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt @@ -81,7 +81,12 @@ class ChapterRepositoryImpl( } override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List { - return handler.awaitList { chaptersQueries.getBookmarkedChaptersByMangaId(mangaId, chapterMapper) } + return handler.awaitList { + chaptersQueries.getBookmarkedChaptersByMangaId( + mangaId, + chapterMapper, + ) + } } override suspend fun getChapterById(id: Long): Chapter? { @@ -89,10 +94,21 @@ class ChapterRepositoryImpl( } override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow> { - return handler.subscribeToList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) } + return handler.subscribeToList { + chaptersQueries.getChaptersByMangaId( + mangaId, + chapterMapper, + ) + } } override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? { - return handler.awaitOneOrNull { chaptersQueries.getChapterByUrlAndMangaId(url, mangaId, chapterMapper) } + return handler.awaitOneOrNull { + chaptersQueries.getChapterByUrlAndMangaId( + url, + mangaId, + chapterMapper, + ) + } } } diff --git a/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt b/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt index c2d110f74..134a1c91c 100644 --- a/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt +++ b/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt @@ -14,7 +14,19 @@ val historyMapper: (Long, Long, Date?, Long) -> History = { id, chapterId, readA ) } -val historyWithRelationsMapper: (Long, Long, Long, String, String?, Long, Boolean, Long, Double, Date?, Long) -> HistoryWithRelations = { +val historyWithRelationsMapper: ( + Long, + Long, + Long, + String, + String?, + Long, + Boolean, + Long, + Double, + Date?, + Long, +) -> HistoryWithRelations = { historyId, mangaId, chapterId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, chapterNumber, readAt, readDuration -> HistoryWithRelations( id = historyId, diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt index 3cc46aa5c..43bb1ed0c 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt @@ -4,7 +4,30 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.manga.model.Manga -val mangaMapper: (Long, Long, String, String?, String?, String?, List?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, UpdateStrategy, Long, Long, Long?) -> Manga = +val mangaMapper: ( + Long, + Long, + String, + String?, + String?, + String?, + List?, + String, + Long, + String?, + Boolean, + Long?, + Long?, + Boolean, + Long, + Long, + Long, + Long, + UpdateStrategy, + Long, + Long, + Long?, +) -> Manga = { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt -> Manga( id = id, @@ -32,7 +55,37 @@ val mangaMapper: (Long, Long, String, String?, String?, String?, List?, ) } -val libraryManga: (Long, Long, String, String?, String?, String?, List?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, UpdateStrategy, Long, Long, Long?, Long, Double, Long, Long, Long, Double, Long) -> LibraryManga = +val libraryManga: ( + Long, + Long, + String, + String?, + String?, + String?, + List?, + String, + Long, + String?, + Boolean, + Long?, + Long?, + Boolean, + Long, + Long, + Long, + Long, + UpdateStrategy, + Long, + Long, + Long?, + Long, + Double, + Long, + Long, + Long, + Double, + Long, +) -> LibraryManga = { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, calculateInterval, lastModifiedAt, favoriteModifiedAt, totalCount, readCount, latestUpload, chapterFetchedAt, lastRead, bookmarkCount, category -> LibraryManga( manga = mangaMapper( diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index 49f3160a6..39bbcb99f 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -24,11 +24,23 @@ class MangaRepositoryImpl( } override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? { - return handler.awaitOneOrNull(inTransaction = true) { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) } + return handler.awaitOneOrNull(inTransaction = true) { + mangasQueries.getMangaByUrlAndSource( + url, + sourceId, + mangaMapper, + ) + } } override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow { - return handler.subscribeToOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) } + return handler.subscribeToOneOrNull { + mangasQueries.getMangaByUrlAndSource( + url, + sourceId, + mangaMapper, + ) + } } override suspend fun getFavorites(): List { diff --git a/data/src/main/java/tachiyomi/data/release/GithubRelease.kt b/data/src/main/java/tachiyomi/data/release/GithubRelease.kt index 94394cef0..f7fa16923 100644 --- a/data/src/main/java/tachiyomi/data/release/GithubRelease.kt +++ b/data/src/main/java/tachiyomi/data/release/GithubRelease.kt @@ -32,7 +32,9 @@ data class GitHubAssets(@SerialName("browser_download_url") val downloadLink: St * Reference: https://stackoverflow.com/a/30281147 */ val gitHubUsernameMentionRegex = - """\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))""".toRegex(RegexOption.IGNORE_CASE) + """\B@([a-z0-9](?:-(?=[a-z0-9])|[a-z0-9]){0,38}(?<=[a-z0-9]))""".toRegex( + RegexOption.IGNORE_CASE, + ) val releaseMapper: (GithubRelease) -> Release = { Release( diff --git a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt index 032ae0ab5..c87ec5cff 100644 --- a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt @@ -5,25 +5,26 @@ import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga -import tachiyomi.core.util.lang.awaitSingle import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.source.repository.SourcePagingSourceType -class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource(source) { +class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource( + source, +) { override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.fetchSearchManga(currentPage, query, filters).awaitSingle() + return source.getSearchManga(currentPage, query, filters) } } class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.fetchPopularManga(currentPage).awaitSingle() + return source.getPopularManga(currentPage) } } class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) { override suspend fun requestNextPage(currentPage: Int): MangasPage { - return source.fetchLatestUpdates(currentPage).awaitSingle() + return source.getLatestUpdates(currentPage) } } diff --git a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt index e8978e25b..a4a9714f6 100644 --- a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt +++ b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt @@ -2,7 +2,21 @@ package tachiyomi.data.track import tachiyomi.domain.track.model.Track -val trackMapper: (Long, Long, Long, Long, Long?, String, Double, Long, Long, Double, String, Long, Long) -> Track = +val trackMapper: ( + Long, + Long, + Long, + Long, + Long?, + String, + Double, + Long, + Long, + Double, + String, + Long, + Long, +) -> Track = { id, mangaId, syncId, remoteId, libraryId, title, lastChapterRead, totalChapters, status, score, remoteUrl, startDate, finishDate -> Track( id = id, diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesMapper.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesMapper.kt index efbc55410..56e9743a9 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesMapper.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesMapper.kt @@ -3,7 +3,22 @@ package tachiyomi.data.updates import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.updates.model.UpdatesWithRelations -val updateWithRelationMapper: (Long, String, Long, String, String?, Boolean, Boolean, Long, Long, Boolean, String?, Long, Long, Long) -> UpdatesWithRelations = { +val updateWithRelationMapper: ( + Long, + String, + Long, + String, + String?, + Boolean, + Boolean, + Long, + Long, + Boolean, + String?, + Long, + Long, + Long, +) -> UpdatesWithRelations = { mangaId, mangaTitle, chapterId, chapterName, scanlator, read, bookmark, lastPageRead, sourceId, favorite, thumbnailUrl, coverLastModified, _, dateFetch -> UpdatesWithRelations( mangaId = mangaId, diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt index 00b4fffcf..b50aee4be 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt @@ -9,11 +9,16 @@ class UpdatesRepositoryImpl( private val databaseHandler: DatabaseHandler, ) : UpdatesRepository { - override suspend fun awaitWithRead(read: Boolean, after: Long): List { + override suspend fun awaitWithRead( + read: Boolean, + after: Long, + limit: Long, + ): List { return databaseHandler.awaitList { updatesViewQueries.getUpdatesByReadStatus( read = read, after = after, + limit = limit, mapper = updateWithRelationMapper, ) } @@ -25,11 +30,16 @@ class UpdatesRepositoryImpl( } } - override fun subscribeWithRead(read: Boolean, after: Long): Flow> { + override fun subscribeWithRead( + read: Boolean, + after: Long, + limit: Long, + ): Flow> { return databaseHandler.subscribeToList { updatesViewQueries.getUpdatesByReadStatus( read = read, after = after, + limit = limit, mapper = updateWithRelationMapper, ) } diff --git a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq index 6e9fd95c2..5fdd3ea75 100644 --- a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq @@ -30,4 +30,5 @@ getUpdatesByReadStatus: SELECT * FROM updatesView WHERE read = :read -AND dateUpload > :after; \ No newline at end of file +AND dateUpload > :after +LIMIT :limit; \ No newline at end of file diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt index af61400ab..7561f52c8 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt @@ -16,11 +16,9 @@ class ReorderCategory( private val mutex = Mutex() - suspend fun moveUp(category: Category): Result = - await(category, MoveTo.UP) + suspend fun moveUp(category: Category): Result = await(category, MoveTo.UP) - suspend fun moveDown(category: Category): Result = - await(category, MoveTo.DOWN) + suspend fun moveDown(category: Category): Result = await(category, MoveTo.DOWN) private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext { mutex.withLock { @@ -57,6 +55,27 @@ class ReorderCategory( } } + suspend fun sortAlphabetically() = withNonCancellableContext { + mutex.withLock { + val updates = categoryRepository.getAll() + .sortedBy { category -> category.name } + .mapIndexed { index, category -> + CategoryUpdate( + id = category.id, + order = index.toLong(), + ) + } + + try { + categoryRepository.updatePartial(updates) + Result.Success + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + Result.InternalError(e) + } + } + } + sealed interface Result { data object Success : Result data object Unchanged : Result diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/SetSortModeForCategory.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/SetSortModeForCategory.kt index 28c999e9f..e514e0898 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/SetSortModeForCategory.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/SetSortModeForCategory.kt @@ -28,7 +28,11 @@ class SetSortModeForCategory( } } - suspend fun await(category: Category?, type: LibrarySort.Type, direction: LibrarySort.Direction) { + suspend fun await( + category: Category?, + type: LibrarySort.Type, + direction: LibrarySort.Direction, + ) { await(category?.id, type, direction) } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/SetDefaultChapterSettings.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/SetMangaDefaultChapterFlags.kt similarity index 100% rename from domain/src/main/java/tachiyomi/domain/chapter/interactor/SetDefaultChapterSettings.kt rename to domain/src/main/java/tachiyomi/domain/chapter/interactor/SetMangaDefaultChapterFlags.kt diff --git a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt index d41eb1484..b2c3f6b44 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterRecognition.kt @@ -30,7 +30,11 @@ object ChapterRecognition { */ private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""") - fun parseChapterNumber(mangaTitle: String, chapterName: String, chapterNumber: Double? = null): Double { + fun parseChapterNumber( + mangaTitle: String, + chapterName: String, + chapterNumber: Double? = null, + ): Double { // If chapter number is known return. if (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) { return chapterNumber diff --git a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt index e7f5648cb..45f302455 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/service/ChapterSort.kt @@ -3,7 +3,13 @@ package tachiyomi.domain.chapter.service import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga -fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int { +fun getChapterSort( + manga: Manga, + sortDescending: Boolean = manga.sortDescending(), +): ( + Chapter, + Chapter, +) -> Int { return when (manga.sorting) { Manga.CHAPTER_SORTING_SOURCE -> when (sortDescending) { true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) } diff --git a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt index cc930cf51..51a07dc8e 100644 --- a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt @@ -8,9 +8,15 @@ class DownloadPreferences( private val preferenceStore: PreferenceStore, ) { - fun downloadsDirectory() = preferenceStore.getString("download_directory", folderProvider.path()) + fun downloadsDirectory() = preferenceStore.getString( + "download_directory", + folderProvider.path(), + ) - fun downloadOnlyOverWifi() = preferenceStore.getBoolean("pref_download_only_over_wifi_key", true) + fun downloadOnlyOverWifi() = preferenceStore.getBoolean( + "pref_download_only_over_wifi_key", + true, + ) fun saveChaptersAsCBZ() = preferenceStore.getBoolean("save_chapter_as_cbz", true) @@ -20,15 +26,27 @@ class DownloadPreferences( fun removeAfterReadSlots() = preferenceStore.getInt("remove_after_read_slots", -1) - fun removeAfterMarkedAsRead() = preferenceStore.getBoolean("pref_remove_after_marked_as_read_key", false) + fun removeAfterMarkedAsRead() = preferenceStore.getBoolean( + "pref_remove_after_marked_as_read_key", + false, + ) fun removeBookmarkedChapters() = preferenceStore.getBoolean("pref_remove_bookmarked", false) - fun removeExcludeCategories() = preferenceStore.getStringSet("remove_exclude_categories", emptySet()) + fun removeExcludeCategories() = preferenceStore.getStringSet( + "remove_exclude_categories", + emptySet(), + ) fun downloadNewChapters() = preferenceStore.getBoolean("download_new", false) - fun downloadNewChapterCategories() = preferenceStore.getStringSet("download_new_categories", emptySet()) + fun downloadNewChapterCategories() = preferenceStore.getStringSet( + "download_new_categories", + emptySet(), + ) - fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet("download_new_categories_exclude", emptySet()) + fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet( + "download_new_categories_exclude", + emptySet(), + ) } diff --git a/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt b/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt index ebe6fa83e..6e9526158 100644 --- a/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt +++ b/domain/src/main/java/tachiyomi/domain/history/interactor/GetNextChapters.kt @@ -30,7 +30,11 @@ class GetNextChapters( } } - suspend fun await(mangaId: Long, fromChapterId: Long, onlyUnread: Boolean = true): List { + suspend fun await( + mangaId: Long, + fromChapterId: Long, + onlyUnread: Boolean = true, + ): List { val chapters = await(mangaId, onlyUnread) val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId } val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size) diff --git a/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt b/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt index 40acc5b5a..7f525eb57 100644 --- a/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt +++ b/domain/src/main/java/tachiyomi/domain/library/model/LibrarySortMode.kt @@ -65,7 +65,18 @@ data class LibrarySort( } companion object { - val types by lazy { setOf(Type.Alphabetical, Type.LastRead, Type.LastUpdate, Type.UnreadCount, Type.TotalChapters, Type.LatestChapter, Type.ChapterFetchDate, Type.DateAdded) } + val types by lazy { + setOf( + Type.Alphabetical, + Type.LastRead, + Type.LastUpdate, + Type.UnreadCount, + Type.TotalChapters, + Type.LatestChapter, + Type.ChapterFetchDate, + Type.DateAdded, + ) + } val directions by lazy { setOf(Direction.Ascending, Direction.Descending) } val default = LibrarySort(Type.Alphabetical, Direction.Ascending) diff --git a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt index e66c705d9..8c853c759 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -11,9 +11,19 @@ class LibraryPreferences( private val preferenceStore: PreferenceStore, ) { - fun displayMode() = preferenceStore.getObject("pref_display_mode_library", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize) + fun displayMode() = preferenceStore.getObject( + "pref_display_mode_library", + LibraryDisplayMode.default, + LibraryDisplayMode.Serializer::serialize, + LibraryDisplayMode.Serializer::deserialize, + ) - fun sortingMode() = preferenceStore.getObject("library_sorting_mode", LibrarySort.default, LibrarySort.Serializer::serialize, LibrarySort.Serializer::deserialize) + fun sortingMode() = preferenceStore.getObject( + "library_sorting_mode", + LibrarySort.default, + LibrarySort.Serializer::serialize, + LibrarySort.Serializer::deserialize, + ) fun portraitColumns() = preferenceStore.getInt("pref_library_columns_portrait_key", 0) @@ -42,31 +52,64 @@ class LibraryPreferences( fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false) - fun showContinueReadingButton() = preferenceStore.getBoolean("display_continue_reading_button", false) + fun showContinueReadingButton() = preferenceStore.getBoolean( + "display_continue_reading_button", + false, + ) // region Filter - fun filterDownloaded() = preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriState.DISABLED) + fun filterDownloaded() = preferenceStore.getEnum( + "pref_filter_library_downloaded_v2", + TriState.DISABLED, + ) fun filterUnread() = preferenceStore.getEnum("pref_filter_library_unread_v2", TriState.DISABLED) - fun filterStarted() = preferenceStore.getEnum("pref_filter_library_started_v2", TriState.DISABLED) + fun filterStarted() = preferenceStore.getEnum( + "pref_filter_library_started_v2", + TriState.DISABLED, + ) - fun filterBookmarked() = preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriState.DISABLED) + fun filterBookmarked() = preferenceStore.getEnum( + "pref_filter_library_bookmarked_v2", + TriState.DISABLED, + ) - fun filterCompleted() = preferenceStore.getEnum("pref_filter_library_completed_v2", TriState.DISABLED) + fun filterCompleted() = preferenceStore.getEnum( + "pref_filter_library_completed_v2", + TriState.DISABLED, + ) - fun filterIntervalCustom() = preferenceStore.getEnum("pref_filter_library_interval_custom", TriState.DISABLED) + fun filterIntervalCustom() = preferenceStore.getEnum( + "pref_filter_library_interval_custom", + TriState.DISABLED, + ) - fun filterIntervalLong() = preferenceStore.getEnum("pref_filter_library_interval_long", TriState.DISABLED) + fun filterIntervalLong() = preferenceStore.getEnum( + "pref_filter_library_interval_long", + TriState.DISABLED, + ) - fun filterIntervalLate() = preferenceStore.getEnum("pref_filter_library_interval_late", TriState.DISABLED) + fun filterIntervalLate() = preferenceStore.getEnum( + "pref_filter_library_interval_late", + TriState.DISABLED, + ) - fun filterIntervalDropped() = preferenceStore.getEnum("pref_filter_library_interval_dropped", TriState.DISABLED) + fun filterIntervalDropped() = preferenceStore.getEnum( + "pref_filter_library_interval_dropped", + TriState.DISABLED, + ) - fun filterIntervalPassed() = preferenceStore.getEnum("pref_filter_library_interval_passed", TriState.DISABLED) + fun filterIntervalPassed() = preferenceStore.getEnum( + "pref_filter_library_interval_passed", + TriState.DISABLED, + ) - fun filterTracking(id: Int) = preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriState.DISABLED) + fun filterTracking(id: Int) = preferenceStore.getEnum( + "pref_filter_library_tracked_${id}_v2", + TriState.DISABLED, + ) // endregion @@ -97,24 +140,45 @@ class LibraryPreferences( fun updateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet()) - fun updateCategoriesExclude() = preferenceStore.getStringSet("library_update_categories_exclude", emptySet()) + fun updateCategoriesExclude() = preferenceStore.getStringSet( + "library_update_categories_exclude", + emptySet(), + ) // endregion // region Chapter - fun filterChapterByRead() = preferenceStore.getLong("default_chapter_filter_by_read", Manga.SHOW_ALL) + fun filterChapterByRead() = preferenceStore.getLong( + "default_chapter_filter_by_read", + Manga.SHOW_ALL, + ) - fun filterChapterByDownloaded() = preferenceStore.getLong("default_chapter_filter_by_downloaded", Manga.SHOW_ALL) + fun filterChapterByDownloaded() = preferenceStore.getLong( + "default_chapter_filter_by_downloaded", + Manga.SHOW_ALL, + ) - fun filterChapterByBookmarked() = preferenceStore.getLong("default_chapter_filter_by_bookmarked", Manga.SHOW_ALL) + fun filterChapterByBookmarked() = preferenceStore.getLong( + "default_chapter_filter_by_bookmarked", + Manga.SHOW_ALL, + ) // and upload date - fun sortChapterBySourceOrNumber() = preferenceStore.getLong("default_chapter_sort_by_source_or_number", Manga.CHAPTER_SORTING_SOURCE) + fun sortChapterBySourceOrNumber() = preferenceStore.getLong( + "default_chapter_sort_by_source_or_number", + Manga.CHAPTER_SORTING_SOURCE, + ) - fun displayChapterByNameOrNumber() = preferenceStore.getLong("default_chapter_display_by_name_or_number", Manga.CHAPTER_DISPLAY_NAME) + fun displayChapterByNameOrNumber() = preferenceStore.getLong( + "default_chapter_display_by_name_or_number", + Manga.CHAPTER_DISPLAY_NAME, + ) - fun sortChapterByAscendingOrDescending() = preferenceStore.getLong("default_chapter_sort_by_ascending_or_descending", Manga.CHAPTER_SORT_DESC) + fun sortChapterByAscendingOrDescending() = preferenceStore.getLong( + "default_chapter_sort_by_ascending_or_descending", + Manga.CHAPTER_SORT_DESC, + ) fun setChapterSettingsDefault(manga: Manga) { filterChapterByRead().set(manga.unreadFilterRaw) @@ -122,7 +186,9 @@ class LibraryPreferences( filterChapterByBookmarked().set(manga.bookmarkedFilterRaw) sortChapterBySourceOrNumber().set(manga.sorting) displayChapterByNameOrNumber().set(manga.displayMode) - sortChapterByAscendingOrDescending().set(if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC) + sortChapterByAscendingOrDescending().set( + if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC, + ) } fun autoClearChapterCache() = preferenceStore.getBoolean("auto_clear_chapter_cache", false) @@ -131,9 +197,15 @@ class LibraryPreferences( // region Swipe Actions - fun swipeToStartAction() = preferenceStore.getEnum("pref_chapter_swipe_end_action", ChapterSwipeAction.ToggleBookmark) + fun swipeToStartAction() = preferenceStore.getEnum( + "pref_chapter_swipe_end_action", + ChapterSwipeAction.ToggleBookmark, + ) - fun swipeToEndAction() = preferenceStore.getEnum("pref_chapter_swipe_start_action", ChapterSwipeAction.ToggleRead) + fun swipeToEndAction() = preferenceStore.getEnum( + "pref_chapter_swipe_start_action", + ChapterSwipeAction.ToggleRead, + ) // endregion diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/SetFetchInterval.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt similarity index 80% rename from domain/src/main/java/tachiyomi/domain/manga/interactor/SetFetchInterval.kt rename to domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt index 6dc77819e..740c0a150 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/SetFetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt @@ -5,14 +5,12 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate import java.time.Instant +import java.time.ZoneId import java.time.ZonedDateTime import java.time.temporal.ChronoUnit import kotlin.math.absoluteValue -const val MAX_FETCH_INTERVAL = 28 -private const val FETCH_INTERVAL_GRACE_PERIOD = 1 - -class SetFetchInterval( +class FetchInterval( private val getChapterByMangaId: GetChapterByMangaId, ) { @@ -27,7 +25,10 @@ class SetFetchInterval( window } val chapters = getChapterByMangaId.await(manga.id) - val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime) + val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval( + chapters, + dateTime.zone, + ) val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow) return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) { @@ -39,31 +40,34 @@ class SetFetchInterval( fun getWindow(dateTime: ZonedDateTime): Pair { val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone) - val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong()) - val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong()) + val lowerBound = today.minusDays(GRACE_PERIOD) + val upperBound = today.plusDays(GRACE_PERIOD) return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1) } - internal fun calculateInterval(chapters: List, zonedDateTime: ZonedDateTime): Int { - val sortedChapters = chapters - .sortedWith(compareByDescending { it.dateUpload }.thenByDescending { it.dateFetch }) - .take(50) - - val uploadDates = sortedChapters + internal fun calculateInterval(chapters: List, zone: ZoneId): Int { + val uploadDates = chapters.asSequence() .filter { it.dateUpload > 0L } + .sortedByDescending { it.dateUpload } .map { - ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone) + ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone) .toLocalDate() .atStartOfDay() } .distinct() - val fetchDates = sortedChapters + .take(10) + .toList() + + val fetchDates = chapters.asSequence() + .sortedByDescending { it.dateFetch } .map { - ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone) + ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone) .toLocalDate() .atStartOfDay() } .distinct() + .take(10) + .toList() val interval = when { // Enough upload date from source @@ -82,7 +86,7 @@ class SetFetchInterval( else -> 7 } - return interval.coerceIn(1, MAX_FETCH_INTERVAL) + return interval.coerceIn(1, MAX_INTERVAL) } private fun calculateNextUpdate( @@ -95,7 +99,10 @@ class SetFetchInterval( manga.nextUpdate !in window.first.rangeTo(window.second + 1) || manga.fetchInterval == 0 ) { - val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone) + val latestDate = ZonedDateTime.ofInstant( + Instant.ofEpochMilli(manga.lastUpdate), + dateTime.zone, + ) .toLocalDate() .atStartOfDay() val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt() @@ -110,7 +117,7 @@ class SetFetchInterval( } private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int { - if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL + if (delta >= MAX_INTERVAL) return MAX_INTERVAL // double delta again if missed more than 9 check in new delta val cycle = timeSinceLatest.floorDiv(delta) + 1 @@ -120,4 +127,10 @@ class SetFetchInterval( delta } } + + companion object { + const val MAX_INTERVAL = 28 + + private const val GRACE_PERIOD = 1L + } } diff --git a/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt b/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt index 735782fd2..1a0ff4e4b 100644 --- a/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt +++ b/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt @@ -20,7 +20,10 @@ class GetApplicationRelease( val now = Instant.now() // Limit checks to once every 3 days at most - if (arguments.forceCheck.not() && now.isBefore(Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS))) { + if (arguments.forceCheck.not() && now.isBefore( + Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS), + ) + ) { return Result.NoNewUpdate } @@ -29,7 +32,12 @@ class GetApplicationRelease( lastChecked.set(now.toEpochMilli()) // Check if latest version is different from current version - val isNewVersion = isNewVersion(arguments.isPreview, arguments.commitCount, arguments.versionName, release.version) + val isNewVersion = isNewVersion( + arguments.isPreview, + arguments.commitCount, + arguments.versionName, + release.version, + ) return when { isNewVersion && arguments.isThirdParty -> Result.ThirdPartyInstallation isNewVersion -> Result.NewUpdate(release) @@ -37,7 +45,12 @@ class GetApplicationRelease( } } - private fun isNewVersion(isPreview: Boolean, commitCount: Int, versionName: String, versionTag: String): Boolean { + private fun isNewVersion( + isPreview: Boolean, + commitCount: Int, + versionName: String, + versionTag: String, + ): Boolean { // Removes prefixes like "r" or "v" val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") return if (isPreview) { diff --git a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt index 38d38ef57..8b5f67ef5 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/StubSource.kt @@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import rx.Observable class StubSource( override val id: Long, @@ -14,36 +13,16 @@ class StubSource( private val isInvalid: Boolean = name.isBlank() || lang.isBlank() - override suspend fun getMangaDetails(manga: SManga): SManga { + override suspend fun getMangaDetails(manga: SManga): SManga = throw SourceNotInstalledException() - } - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails")) - override fun fetchMangaDetails(manga: SManga): Observable { - return Observable.error(SourceNotInstalledException()) - } - - override suspend fun getChapterList(manga: SManga): List { + override suspend fun getChapterList(manga: SManga): List = throw SourceNotInstalledException() - } - - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList")) - override fun fetchChapterList(manga: SManga): Observable> { - return Observable.error(SourceNotInstalledException()) - } - - override suspend fun getPageList(chapter: SChapter): List { + override suspend fun getPageList(chapter: SChapter): List = throw SourceNotInstalledException() - } - @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) - override fun fetchPageList(chapter: SChapter): Observable> { - return Observable.error(SourceNotInstalledException()) - } - - override fun toString(): String { - return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString() - } + override fun toString(): String = + if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString() } class SourceNotInstalledException : Exception() diff --git a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt index 0c1ac5f83..b3c6481d5 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt @@ -10,7 +10,7 @@ class GetUpdates( ) { suspend fun await(read: Boolean, after: Long): List { - return repository.awaitWithRead(read, after) + return repository.awaitWithRead(read, after, limit = 500) } fun subscribe(calendar: Calendar): Flow> { @@ -18,6 +18,6 @@ class GetUpdates( } fun subscribe(read: Boolean, after: Long): Flow> { - return repository.subscribeWithRead(read, after) + return repository.subscribeWithRead(read, after, limit = 500) } } diff --git a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt index 0856b4b9f..3b583ea90 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt @@ -5,9 +5,9 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations interface UpdatesRepository { - suspend fun awaitWithRead(read: Boolean, after: Long): List + suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List fun subscribeAll(after: Long, limit: Long): Flow> - fun subscribeWithRead(read: Boolean, after: Long): Flow> + fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow> } diff --git a/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt b/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt index 633ba250e..408dee18a 100644 --- a/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt +++ b/domain/src/test/java/tachiyomi/domain/library/model/LibraryFlagsTest.kt @@ -34,7 +34,10 @@ class LibraryFlagsTest { @Test fun `Test Flag plus operator with old flag as base`() { - val currentSort = LibrarySort(LibrarySort.Type.UnreadCount, LibrarySort.Direction.Descending) + val currentSort = LibrarySort( + LibrarySort.Type.UnreadCount, + LibrarySort.Direction.Descending, + ) currentSort.flag shouldBe 0b00001100 val sort = LibrarySort(LibrarySort.Type.DateAdded, LibrarySort.Direction.Ascending) diff --git a/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt new file mode 100644 index 000000000..468d7eb2d --- /dev/null +++ b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt @@ -0,0 +1,127 @@ +package tachiyomi.domain.manga.interactor + +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode +import tachiyomi.domain.chapter.model.Chapter +import java.time.ZoneOffset +import java.time.ZonedDateTime +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import kotlin.time.toJavaDuration + +@Execution(ExecutionMode.CONCURRENT) +class FetchIntervalTest { + + private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z") + private val testZoneId = ZoneOffset.UTC + private var chapter = Chapter.create().copy( + dateFetch = testTime.toEpochSecond() * 1000, + dateUpload = testTime.toEpochSecond() * 1000, + ) + + private val fetchInterval = FetchInterval(mockk()) + + @Test + fun `returns default interval of 7 days when not enough distinct days`() { + val chaptersWithUploadDate = (1..50).map { + chapterWithTime(chapter, 1.days) + } + fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 7 + + val chaptersWithoutUploadDate = chaptersWithUploadDate.map { + it.copy(dateUpload = 0L) + } + fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 7 + } + + @Test + fun `returns interval based on more recent chapters`() { + val oldChapters = (1..5).map { + chapterWithTime(chapter, (it * 7).days) // Would have interval of 7 days + } + val newChapters = (1..10).map { + chapterWithTime(chapter, oldChapters.lastUploadDate() + it.days) + } + + val chapters = oldChapters + newChapters + + fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 + } + + @Test + fun `returns interval of 7 days when multiple chapters in 1 day`() { + val chapters = (1..10).map { + chapterWithTime(chapter, 10.hours) + } + fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7 + } + + @Test + fun `returns interval of 7 days when multiple chapters in 2 days`() { + val chapters = (1..2).map { + chapterWithTime(chapter, 1.days) + } + (1..5).map { + chapterWithTime(chapter, 2.days) + } + fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7 + } + + @Test + fun `returns interval of 1 day when chapters are released every 1 day`() { + val chapters = (1..20).map { + chapterWithTime(chapter, it.days) + } + fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 + } + + @Test + fun `returns interval of 1 day when delta is less than 1 day`() { + val chapters = (1..20).map { + chapterWithTime(chapter, (15 * it).hours) + } + fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 + } + + @Test + fun `returns interval of 2 days when chapters are released every 2 days`() { + val chapters = (1..20).map { + chapterWithTime(chapter, (2 * it).days) + } + fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2 + } + + @Test + fun `returns interval with floored value when interval is decimal`() { + val chaptersWithUploadDate = (1..5).map { + chapterWithTime(chapter, (25 * it).hours) + } + fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 1 + + val chaptersWithoutUploadDate = chaptersWithUploadDate.map { + it.copy(dateUpload = 0L) + } + fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 1 + } + + @Test + fun `returns interval of 1 day when chapters are released just below every 2 days`() { + val chapters = (1..20).map { + chapterWithTime(chapter, (43 * it).hours) + } + fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1 + } + + private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter { + val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000 + return chapter.copy(dateFetch = newTime, dateUpload = newTime) + } + + private fun List.lastUploadDate() = + last().dateUpload.toDuration(DurationUnit.MILLISECONDS) +} diff --git a/domain/src/test/java/tachiyomi/domain/manga/interactor/SetFetchIntervalTest.kt b/domain/src/test/java/tachiyomi/domain/manga/interactor/SetFetchIntervalTest.kt deleted file mode 100644 index 8c329ca22..000000000 --- a/domain/src/test/java/tachiyomi/domain/manga/interactor/SetFetchIntervalTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package tachiyomi.domain.manga.interactor - -import io.kotest.matchers.shouldBe -import io.mockk.mockk -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.parallel.Execution -import org.junit.jupiter.api.parallel.ExecutionMode -import tachiyomi.domain.chapter.model.Chapter -import java.time.ZonedDateTime -import kotlin.time.Duration -import kotlin.time.Duration.Companion.hours -import kotlin.time.toJavaDuration - -@Execution(ExecutionMode.CONCURRENT) -class SetFetchIntervalTest { - - private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z") - private var chapter = Chapter.create().copy( - dateFetch = testTime.toEpochSecond() * 1000, - dateUpload = testTime.toEpochSecond() * 1000, - ) - - private val setFetchInterval = SetFetchInterval(mockk()) - - @Test - fun `calculateInterval returns default of 7 days when less than 3 distinct days`() { - val chapters = (1..2).map { - chapterWithTime(chapter, 10.hours) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7 - } - - @Test - fun `calculateInterval returns 7 when 5 chapters in 1 day`() { - val chapters = (1..5).map { - chapterWithTime(chapter, 10.hours) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7 - } - - @Test - fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() { - val chapters = (1..2).map { - chapterWithTime(chapter, 24.hours) - } + (1..5).map { - chapterWithTime(chapter, 48.hours) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7 - } - - @Test - fun `calculateInterval returns default of 1 day when interval less than 1`() { - val chapters = (1..5).map { - chapterWithTime(chapter, (15 * it).hours) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 - } - - // Normal interval calculation - @Test - fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() { - val chapters = (1..5).map { - chapterWithTime(chapter, (24 * it).hours) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 - } - - @Test - fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() { - val chapters = (1..5).map { - chapterWithTime(chapter, (48 * it).hours) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 2 - } - - @Test - fun `calculateInterval returns floored value when interval is decimal`() { - val chapters = (1..5).map { - chapterWithTime(chapter, (25 * it).hours) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 - } - - @Test - fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() { - val chapters = (1..5).map { - chapterWithTime(chapter, (43 * it).hours) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 - } - - @Test - fun `calculateInterval returns interval based on fetch time if upload time not available`() { - val chapters = (1..5).map { - chapterWithTime(chapter, (25 * it).hours).copy(dateUpload = 0L) - } - setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 - } - - private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter { - val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000 - return chapter.copy(dateFetch = newTime, dateUpload = newTime) - } -} diff --git a/domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt b/domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt index 41df15221..f813094fe 100644 --- a/domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt +++ b/domain/src/test/java/tachiyomi/domain/release/interactor/GetApplicationReleaseTest.kt @@ -79,7 +79,9 @@ class GetApplicationReleaseTest { ), ) - (result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(release).release + (result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate( + release, + ).release } @Test @@ -106,7 +108,9 @@ class GetApplicationReleaseTest { ), ) - (result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(release).release + (result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate( + release, + ).release } @Test diff --git a/gradle.properties b/gradle.properties index 8102dd176..282f16ede 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,5 +24,4 @@ kotlin.mpp.androidSourceSetLayoutVersion=2 android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false -#android.nonFinalResIds=false \ No newline at end of file +android.nonTransitiveRClass=false \ No newline at end of file diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 9fe6455c3..f9db071ec 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,20 +1,20 @@ [versions] -agp_version = "8.1.1" -lifecycle_version = "2.6.1" -paging_version = "3.2.0" +agp_version = "8.1.2" +lifecycle_version = "2.6.2" +paging_version = "3.2.1" [libraries] gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" } -annotation = "androidx.annotation:annotation:1.7.0-rc01" +annotation = "androidx.annotation:annotation:1.7.0" appcompat = "androidx.appcompat:appcompat:1.6.1" biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05" constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" -corektx = "androidx.core:core-ktx:1.12.0-rc01" +corektx = "androidx.core:core-ktx:1.12.0" splashscreen = "androidx.core:core-splashscreen:1.0.1" recyclerview = "androidx.recyclerview:recyclerview:1.3.1" viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01" -glance = "androidx.glance:glance-appwidget:1.0.0-rc01" +glance = "androidx.glance:glance-appwidget:1.0.0" profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.1" lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle_version" } @@ -27,7 +27,7 @@ guava = "com.google.guava:guava:32.1.2-android" paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" } paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" } -benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-beta04" +benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-rc02" test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01" test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01" test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04" diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index e21536263..26f0809d0 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,10 +1,10 @@ [versions] -compiler = "1.5.2" +compiler = "1.5.3" compose-bom = "2023.09.00-alpha02" accompanist = "0.33.1-alpha" [libraries] -activity = "androidx.activity:activity-compose:1.7.2" +activity = "androidx.activity:activity-compose:1.8.0" bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" } foundation = { module = "androidx.compose.foundation:foundation" } animation = { module = "androidx.compose.animation:animation" } diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 61d4968c4..1f7a93033 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,7 +1,7 @@ [versions] -kotlin_version = "1.9.0" +kotlin_version = "1.9.10" serialization_version = "1.6.0" -xml_serialization_version = "0.86.1" +xml_serialization_version = "0.86.2" [libraries] reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0cd15c4fa..6b353c453 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,17 @@ [versions] -aboutlib_version = "10.8.3" +aboutlib_version = "10.9.1" okhttp_version = "5.0.0-alpha.11" shizuku_version = "12.2.0" sqlite = "2.3.1" sqldelight = "2.0.0" leakcanary = "2.12" -voyager = "1.0.0-rc06" +voyager = "1.0.0-rc07" richtext = "0.17.0" [libraries] desugar = "com.android.tools:desugar_jdk_libs:2.0.3" android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2" -google-services-gradle = "com.google.gms:google-services:4.3.15" +google-services-gradle = "com.google.gms:google-services:4.4.0" rxjava = "io.reactivex:rxjava:1.3.8" flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4" @@ -19,7 +19,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4" okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" } -okio = "com.squareup.okio:okio:3.5.0" +okio = "com.squareup.okio:okio:3.6.0" conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2" @@ -33,7 +33,7 @@ junrar = "com.github.junrar:junrar:7.5.5" sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" } -sqlite-android = "com.github.requery:sqlite-android:3.42.0" +sqlite-android = "com.github.requery:sqlite-android:3.43.0" preferencektx = "androidx.preference:preference-ktx:1.2.1" @@ -44,27 +44,26 @@ coil-core = { module = "io.coil-kt:coil" } coil-gif = { module = "io.coil-kt:coil-gif" } coil-compose = { module = "io.coil-kt:coil-compose" } -subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:c8e2650" -image-decoder = "com.github.tachiyomiorg:image-decoder:16eda64574" +subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:7e57335" +image-decoder = "com.github.tachiyomiorg:image-decoder:fbd6601290" natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" } richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" } -material = "com.google.android.material:material:1.9.0" +material = "com.google.android.material:material:1.10.0" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" photoview = "com.github.chrisbanes:PhotoView:2.3.0" directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" insetter = "dev.chrisbanes.insetter:insetter:0.6.1" -compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.0.6" -compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0" +compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.1.0" swipe = "me.saket.swipe:swipe:1.2.0" logcat = "com.squareup.logcat:logcat:0.1" -acra-http = "ch.acra:acra-http:5.11.1" +acra-http = "ch.acra:acra-http:5.11.2" firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.3.0" aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" } @@ -83,13 +82,14 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" } junit = "org.junit.jupiter:junit-jupiter:5.10.0" -kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2" -mockk = "io.mockk:mockk:1.13.7" +kotest-assertions = "io.kotest:kotest-assertions-core:5.7.2" +mockk = "io.mockk:mockk:1.13.8" voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } +ktlint = "org.jlleitschuh.gradle:ktlint-gradle:11.6.0" google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev197-1.25.0" google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e8..3fa8f862f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/i18n/src/main/res/values-am/strings.xml b/i18n/src/main/res/values-am/strings.xml index 566ebf712..60b800dd0 100644 --- a/i18n/src/main/res/values-am/strings.xml +++ b/i18n/src/main/res/values-am/strings.xml @@ -143,8 +143,6 @@ ማጣሪያ ምናሌ ማስተካከያዎች - ለመውጣት እንደገና ተመልሰው ይጫኑ - ታቺዮሚን ይክፈቱ ኢክስቴንሽኖች ታሪክ መከታተል diff --git a/i18n/src/main/res/values-ar/strings.xml b/i18n/src/main/res/values-ar/strings.xml index 6489e8a5e..09d7ac2fd 100644 --- a/i18n/src/main/res/values-ar/strings.xml +++ b/i18n/src/main/res/values-ar/strings.xml @@ -112,7 +112,7 @@ اليسار اليمين المركز - نوع التدوير الافتراضي + التدوير الافتراضي حر الوضع الرأسي اﻹجباري الوضع الأفقي الإجباري @@ -127,7 +127,7 @@ من الفصل الرابع قبل اﻷخير من الفصل الخامس قبل اﻷخير تنزيل الفصول الجديدة - الخدمات + المتتبِّعات إنشاء نسخة إحتياطية يمكن استخدامها لإستعادة المكتبة الحالية إستعادة النسخة الإحتياطية @@ -251,7 +251,7 @@ إضافة ذات ريبة هذه اﻹضافة موقَّعة بشهادة ذات ريبة ولم تفعَّل. \n -\nيمكن لأي إضافة خبيثة قراءة بيانات اعتماد تسجيل الدخول المخزَّنة في Tachiyomi أو تنفيذ تعليمات برمجية عشوائية. +\nيمكن لأي إضافة خبيثة قراءة بيانات اعتماد تسجيل الدخول المخزَّنة أو تنفيذ تعليمات برمجية عشوائية. \n \nأنت تقبل هذه المخاطر إن وثقت بالشهادة. سرعة مؤثر النقر المزدوج @@ -297,7 +297,6 @@ تحديث المكتبة المزيد - افتح Tachiyomi نقل إلى الأعلى الأقدم الأَحدثْ @@ -346,11 +345,10 @@ إلغاء التثبيت تثبيت عكس التحديد - اضغط رجوع مرة أخرى للخروج تحديثات اﻹضافات تحديثات الفصول يرجى تحديث تطبيق WebView لتوافق أفضل - يتطلب Tachiyomi وجود WebView + إن وجود WebView لازم ليعمل التطبيق فشل في تجاوز Cloudflare لايوجد تحديث للإضافة @@ -401,7 +399,6 @@ دليل استخدام المصادر المحلية المثبتة آخر مصدر مُستخدم - التحقق من الموقع الإلكتروني في WebView تم تسجيل الخروج تسجيل الخروج تسجيل الخروج من %1$s؟ @@ -441,8 +438,8 @@ المصادر المفقودة: النسخة الإحتياطية لا تحتوي على أيّة إدخالات المكتبة. ملفُّ النسخ الاحتياطيِّ غير صالح - مزامنة أحاديّة لتحديث تقدم الفصول في خدمات التتبع. قم بإعداد التتبع الخاص للإدخالات محدّدة من زر التتبع الخاص بها. - هذه الإضافة ليست من قائمة إضافات Tachiyomi الرسمية. + مزامنة أحادية تُحدِّث قراءة الفصول في المتتبعات الخارجية، ولك تعيين التتبِّع لكلِّ مدخلة على حدى، وذلك من زرِّ التتبع فيهم. + ليست هذه الإضافة من القائمة الرسمية. غير رسمي تحقق من وجود غلاف جديد وتفاصيل جديدة عند تحديث المكتبة تحديث البيانات الوصفية تلقائياً @@ -570,7 +567,7 @@ تاريخ الرفع افقي رأسي - نوع التدوير + التدوير تلقائيًّا إنشاء مجلدات وفقا لعنوان الإدخالات حفظ الصفحات في مجلدات منفصلة @@ -625,8 +622,8 @@ جار تحديث المكتبة…( (%2$d) / (%1$d) ) تقوم بعض الشركات المصنعة بوضع قيود إضافية على التطبيقات التي قد تقضي على الخدمات التي تعمل في الخلفية. يحتوي هذا الموقع على مزيد من المعلومات حول كيفية إصلاحه. قد لا يعمل النسخ الاحتياطيُّ أو الاستعادة إن عطِّلت أمثَلَة MIUI. - الخدمات التي تقدم ميزات محسنة لأجل مصادر معينه. الإدخالات سوف يتم تتبعها عندما يتم إضافتها الي المكتبة. - خدمات محسَّنة + يُحسِّن مصادر معينة، وتُتابَع المدخلات حال إضافتها للمكتبة. + المتتبِّعات المحسَّنة اليوم المصادقة لتأكيد التغيير الافتراضي @@ -646,8 +643,8 @@ دليل البدء ساعد بالترجمة واجهة مستخدم الجهاز اللوحي - فئات المستثناة - المثبت + الفئات المستثناة + المثبِّت إجمالي الإدخالات تحذير اللغة @@ -700,7 +697,6 @@ فتح في GitHub احذف بيانات WebView حُذفت بيانات WebView - عند ارتفاع نسبة الشحن‌ لا يوجد مصادر مثبته لا يوجد مصدر المانجا الغير مقروءة @@ -878,7 +874,14 @@ وُجدت نتائج تعذَّر إنشاء ملفِّ نسخ احتياطيِّ مرخَّصة - لا فصول - ولوج التتبع + ولوج المتتبِّع أُفسد فهرس التنزيلات اضغط هنا تتحرَّ كلاودفلير + لا اتصال بالإنترنت + خطأ HTTP %d، انظر في WebView + لم نصل %s + افتح %s + انقل السلسلة للقعر + التوقيت النسبي + «%1$s» بدلًا عن «%2$s» \ No newline at end of file diff --git a/i18n/src/main/res/values-b+es+419/strings.xml b/i18n/src/main/res/values-b+es+419/strings.xml index 5f1020736..8f4131c86 100644 --- a/i18n/src/main/res/values-b+es+419/strings.xml +++ b/i18n/src/main/res/values-b+es+419/strings.xml @@ -5,7 +5,6 @@ Capítulo más reciente Total de capítulos Información de la extensión - Pulse Atrás de nuevo para salir Copia de seguridad y restauración Historial Historial @@ -23,7 +22,7 @@ Agregar categoría Agregar Editar - Actualizar Biblioteca + Actualizar biblioteca Borrar Descargar Marcar anteriores como leído @@ -38,7 +37,6 @@ Filtrar Menú Configuración - Desbloquear Tachiyomi Capítulos Manga Categorías @@ -85,8 +83,8 @@ Restaurar copia de seguridad Se puede utilizar para restaurar la biblioteca actual Crear copia de seguridad - Sincronización unidireccional para actualizar el progreso del capítulo en los servicios de sincronización. Configure el seguimiento de las entradas de manga individuales desde su botón de seguimiento. - Servicios + Sincronización unidireccional para actualizar el progreso del capítulo en servicios de sincronización externos. Configure el seguimiento de las entradas de manga individuales desde su botón de seguimiento. + Rastreadores Actualizar el progreso después de leer Descargar capítulos nuevos Quinto al último capítulo @@ -109,7 +107,7 @@ Forzar horizontal Forzar vertical Libre - Tipo de rotación predeterminado + Rotación predeterminada Rápido Normal Sin animación @@ -161,11 +159,11 @@ Animar las transiciones de página Mostrar contenido en el área recortada Pantalla completa - Esta extensión no es de la lista oficial de extensiones de Tachiyomi. + Esta extensión no es de la lista oficial. Esta extensión ya no está disponible. Esta puede no funcionar apropiadamente y puede causar problemas en la aplicación. Es recomendable desintalar. Esta extensión fue firmada por una fuente no certificada y no fue activada. \n -\nUna extensión maliciosa puede leer cualquier credencial de inicio guardada en Tachiyomi o ejecutar código arbitrario. +\nUna extensión maliciosa puede leer cualquier credencial de inicio guardada o ejecutar código arbitrario. \n \nAl confiar en este certificado aceptas estos riesgos. Extensión no confiable @@ -217,7 +215,7 @@ No se pudo descargar el capítulo debido a un error inesperado Error Descargador - Se requiere WebView para Tachiyomi + Se requiere WebView para que la aplicación funcione Fallo al evitar Cloudflare Actualización de extensión disponible @@ -320,7 +318,6 @@ Última usada Otro Fuente local - Abrir sitio en WebView No se han encontrado resultados No hay más resultados Pestañas @@ -398,8 +395,8 @@ En curso No hay nuevas actualizaciones disponibles Descargar - Seleccione el archivo de copia de seguridad - Seleccione la imagen de portada + Seleccionar archivo de copia de seguridad + Seleccionar imagen de portada Por favor agregue el manga a la biblioteca antes de hacer esto Error al actualizar la portada @@ -526,7 +523,7 @@ Descarga automática Horizontal En vertical - Tipo de rotación + Rotación Derecha Izquierda Crea carpetas según el título del manga @@ -577,8 +574,8 @@ Algunos fabricantes tienen restricciones adicionales que cierran de forma forzada los servicios en segundo plano. Este sitio web tiene más información sobre cómo arreglarlo. Actividad en segundo plano Rastrear - Servicios que proveen funciones mejoradas para ciertas fuentes. Se hace un seguimiento automático del manga al añadirlo a la biblioteca. - Servicios mejorados + Ofrece funciones mejoradas para ciertas fuentes. Se hace un seguimiento automático del manga al añadirlo a la biblioteca. + Rastreadores mejorados Guia de rastreo Categorías excluidas Más alto @@ -601,7 +598,7 @@ Hoy Modo oscuro con negro puro - Tema Yotsuba + Yotsuba Yin y Yang Turquesa Tako @@ -663,7 +660,6 @@ Una nueva versión esta disponible desde los lanzamientos oficiales, Toca para aprender a emigrar de lanzamientos F-droid no oficiales. Error al guardar imagen Cerrar - Cuando la batería no está baja Abrir en GitHub Ninguna fuente encontrada No se encontró ninguna fuente instalada @@ -826,4 +822,18 @@ %d días Con resultados + Licenciado - No hay capítulos para mostrar + HTTP %d, comprueba la página web en WebView + Sin conexión a Internet + Índice de descargas invalidado + No se pudo crear un archivo de respaldo + Sincronizando biblioteca + Se completó la sincronización de la biblioteca + Toque aquí para obtener ayuda con Cloudflare + Inicio de sesión del rastreador + No se pudo acceder a %s + Desbloquear %s + Mover series al final + Marcas de tiempo relativas + \"%1$s\" en lugar de \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-be/strings.xml b/i18n/src/main/res/values-be/strings.xml index 8aeeb5dfe..1cefa0e7b 100644 --- a/i18n/src/main/res/values-be/strings.xml +++ b/i18n/src/main/res/values-be/strings.xml @@ -69,8 +69,6 @@ Фільтр Меню Налады - Націсніце назад яшчэ раз, каб выйсці - Разблакіраваць Tachiyomi Дапамога Інфармацыя аб пашырэнні Назва @@ -361,7 +359,6 @@ Ёсць непрачытаныя главы Shizuku не працуе Закрыць - калі батарэя зараджана Устаноўка пашырэнняў… Перамясціць у пачатак спісу Усталёўнік diff --git a/i18n/src/main/res/values-bg/strings.xml b/i18n/src/main/res/values-bg/strings.xml index 9e95b4aff..097115845 100644 --- a/i18n/src/main/res/values-bg/strings.xml +++ b/i18n/src/main/res/values-bg/strings.xml @@ -374,7 +374,6 @@ Помощ за локални източници Закачени Последно използвани - Виж уебсайта в WebView Имейл адрес 1 оставаща @@ -429,8 +428,6 @@ Избери обратно Последна глава Меню - Натиснете назад още веднъж, за да излезете - Отключете Тачийоми Източници Още Режим на четене @@ -668,7 +665,6 @@ Не е инсталиран Пропуснати Обърнат портрет - Когато батерията не е паднала Език Случи се неочаквана грешка Автоматично изтегляне при четене diff --git a/i18n/src/main/res/values-bn/strings.xml b/i18n/src/main/res/values-bn/strings.xml index b44956d03..7f3dc3ef9 100644 --- a/i18n/src/main/res/values-bn/strings.xml +++ b/i18n/src/main/res/values-bn/strings.xml @@ -307,8 +307,6 @@ তারিখে যোগকৃত সর্বশেষ অধ্যায় তালিকা - প্রস্থান করতে আবার ব্যাক চাপুন - তাচিওমি খুলুন উৎস সমূহ আরও প্রদর্শন @@ -460,7 +458,6 @@ স্থানীয় উৎস নির্দেশিকা \"%1$s\" সার্বজনীনভাবে খুঁজুন সর্বশেষ ব্যবহৃত - ওয়েবসাইটটি ওয়েবভিউতে পরীক্ষা করুন ট্যাব গুলি ডাউনলোড করা অধ্যায় লাইব্রেরীর মাঙ্গা diff --git a/i18n/src/main/res/values-ca/strings.xml b/i18n/src/main/res/values-ca/strings.xml index 97f260013..898f7c2c5 100644 --- a/i18n/src/main/res/values-ca/strings.xml +++ b/i18n/src/main/res/values-ca/strings.xml @@ -99,7 +99,7 @@ Extensió no fiable Aquesta extensió s’ha signat amb un certificat que no és de confiança i no s’ha activat. \n -\nUna extensió maliciosa podria llegir qualsevol credencial desada al Tachiyomi o executar codi arbitrari. +\nUna extensió maliciosa podria llegir qualsevol credencial d’inici de sessió desada o executar codi arbitrari. \n \nSi confieu en aquest certificat, accepteu aquests riscos. Pantalla completa @@ -136,7 +136,7 @@ Sense animació Normal Ràpida - Tipus de rotació per defecte + Rotació per defecte Lliure Vertical forçada Horitzontal forçada @@ -326,9 +326,8 @@ Actualitzacions pendents Mostra el contingut a l’àrea de retall No s’ha pogut evitar el Cloudflare - Actualitzeu l’aplicació de WebView per a tenir més bona compatibilitat + Actualitzeu l’aplicació WebView per a tenir més bona compatibilitat Novetats de capítols - Desbloca el Tachiyomi El mode discret amaga el contingut de l’aplicació en canviar entre aplicacions i bloca les captures de pantalla Visualització @@ -363,7 +362,6 @@ Hi ha actualitzacions de %d extensions Actualitzacions d’extensions - Comproveu el lloc web en una WebView S’està actualitzant la biblioteca Lectura Omet els capítols filtrats @@ -377,12 +375,11 @@ Desfixa A la biblioteca Afegeix a la biblioteca - El Tachiyomi requereix el WebView + La WebView és necessària per al funcionament de l’aplicació Menys Més Llicències de codi obert Lloc web - Torneu a prémer Endarrere per a sortir Només els baixats Cap. %1$s - %2$s Ha fallat la restauració de la còpia de seguretat @@ -414,7 +411,7 @@ La sincronització és unidireccional per a actualitzar el progrés dels capítols als serveis de seguiment. Configureu el seguiment d’elements individuals al seu botó de seguiment. Refresca les portades de la biblioteca - Aquesta extensió no pertany a la llista d’extensions oficials del Tachiyomi. + Aquesta extensió no pertany a la llista d’extensions oficials. No oficial Per data de pujada Dades @@ -528,7 +525,7 @@ Baixada automàtica Horitzontal Vertical - Tipus de rotació + Rotació Dreta Esquerra Següent @@ -649,10 +646,9 @@ S’ha produït un error en desar la imatge No hi ha cap element a la biblioteca del qual fer còpia de seguretat Hi ha una nova versió disponible als llançaments oficials. Premeu per a obtenir informació de com migrar a partir dels llançaments no oficials d’F-Droid. - Neteja les dades del WebView - S’han netejat les dades del WebView + Neteja les dades de la WebView + S’han netejat les dades de la WebView Tanca - Quan la bateria no sigui baixa No s’ha trobat cap font instal·lada No s’ha trobat cap font Nombre de no llegits @@ -817,4 +813,11 @@ Inici de sessió al seguiment S’ha invalidat l\'índex de baixades Premeu aquí per a obtenir ajuda amb el Cloudflare + Desbloca %s + No hi ha connexió a Internet + HTTP %d, comproveu el lloc web en una WebView + No s’ha pogut accedir a %s + Mou la sèrie a baix de tot + Marques de temps relatives + «%1$s» en comptes de «%2$s» \ No newline at end of file diff --git a/i18n/src/main/res/values-ceb/strings.xml b/i18n/src/main/res/values-ceb/strings.xml index 43f8e0fb7..81529b94c 100644 --- a/i18n/src/main/res/values-ceb/strings.xml +++ b/i18n/src/main/res/values-ceb/strings.xml @@ -2,9 +2,7 @@ Kasaysayan Mga update - I-unlock ang Tachiyomi Panghimatuod aron makumpirma ang pagbag-o - Pindota balik pag-usab aron makagawas Mga setting Menu Gi-bookmark @@ -150,7 +148,6 @@ Kada 2 ka adlaw Kada semana Sa dihang nag-charge - Dili ubos ang baterya Mga pagdili: %s Laktawan ang pag-update sa mga titulo Uban sa wala pa mabasa nga (mga) kapitulo diff --git a/i18n/src/main/res/values-cs/strings.xml b/i18n/src/main/res/values-cs/strings.xml index c8b7b05ce..a1ba82993 100644 --- a/i18n/src/main/res/values-cs/strings.xml +++ b/i18n/src/main/res/values-cs/strings.xml @@ -63,7 +63,7 @@ Nedůvěryhodné rozšíření Toto rozšíření bylo podepsané nedůvěryhodným certifikátem a nebylo aktivované. \n -\nŠkodlivé rozšíření může přečíst přihlašovací údaje uložené v Tachiyomi nebo spustit libovolný kód. +\nŠkodlivé rozšíření může přečíst jakékoliv uložené přihlašovací údaje nebo spustit libovolný kód. \n \nDůvěřováním tomuto certifikátu přijímáte tato rizika. Celá obrazovka @@ -76,7 +76,7 @@ Černá Stránkované (zleva doprava) Stránkované (zprava doleva) - Výchozí typ otáčení + Výchozí otáčení Volné Zamknuto na výšku Zamknuto na šířku @@ -204,7 +204,7 @@ Bez animace Normální Rychle - Služby + Sledovače Vytvořit zálohu Obnovit zálohu Místo zálohy @@ -324,7 +324,6 @@ Nejnovější Zobrazit kapitoly Poslední kapitola - Odemknout Tachiyomi Přidat sledování Nebyly nalezeny žádné kapitoly Nastavit jako výchozí @@ -347,7 +346,6 @@ Hledat \"%1$s\" globálně Připnuté Naposledy použitý - Zobrazit stránku ve WebView Karty Stažené kapitoly Z knihovny @@ -394,7 +392,7 @@ Chybějící zdroje: Záloha neobsahuje žádné položky knihovny. Neplatný soubor se zálohou - Jednosměrná synchronizace pro aktualizaci počtu přečtených kapitol ve sledovacích službách. Nastavení sledování pro každou položku je možné z jejich tlačítka sledování. + Jednosměrná synchronizace pro aktualizaci počtu přečtených kapitol v externích sledovacích službách. Nastavení sledování pro každou položku je možné z jejich tlačítka sledování. Povolení mazání kapitol se záložkami Smazat kapitoly Režim čtení @@ -409,7 +407,7 @@ Žádné Zdroje z tohoto rozšíření mohou obsahovat NSFW (18+) obsah 18+ - Toto rozšíření není z oficiálního seznamu rozšíření pro Tachiyomi. + Toto rozšíření není z oficiálního seznamu rozšíření. Neoficiální %d kategorie @@ -445,7 +443,7 @@ Postup Výchozí nastavení kapitol aktualizováno Prosím aktualizujte aplikaci WebView pro lepší kompatibilitu - Je nutné mít nainstalovanou aplikaci WebView + Je nutné mít nainstalovanou aplikaci WebView pro správné fungování aplikace Nastala chyba při obcházení služby Cloudflare Je dostupná aktualizace rozšíření @@ -499,7 +497,6 @@ Nastavení vyhledávání Datum přidání Sledováno - Opětovným stisknutím tlačítka aplikaci opustíte Režim čtení Ukazovat režim čtení Pro tuto sérii @@ -541,7 +538,7 @@ Datum načtení kapitoly Na šířku Na výšku - Typ otočení + Otáčení Vytváří složky podle názvu položky Uložit stránky do samostatných složek Akce @@ -575,8 +572,8 @@ Aktualizuji knihovnu… (%1$d / %2$d) Někteří výrobci používají další omezení aplikací, která vypíná služby na pozadí. Na této webové stránce najdete další informace o tom, jak to opravit. Pokud je MIUI optimalizace vypnutá, zálohování/obnovování nemusí fungovat správně. - Služby, které poskytují rozšířené funkce pro konkrétní zdroje. Záznamy jsou po přidání do knihovny automaticky sledovány. - Rozšířené služby + Poskytují rozšířené funkce pro konkrétní zdroje. Záznamy jsou po přidání do knihovny automaticky sledovány. + Vylepšené sledovače Návod ke sledování Čistě černý tmavý režim Jotsuba @@ -685,7 +682,6 @@ Posunutí širokých snímků Počet nepřečtených Poslední kontrola aktualizace - Když baterie není vybitá Nelze otevřít poslední přečtenou kapitolu Vlastní obal Nenainstalováno @@ -828,9 +824,16 @@ Má výsledky Synchronizace knihovny Synchronizace knihovny dokončena - Sledování přihlášení + Přihlášení sledovače Klepněte zde pro pomoc s Cloudflare Index stažených zneplatněn Nelze vytvořit soubor zálohy Licencováno - Žádné kapitoly k zobrazení + Bez připojení k internetu + Nelze dosáhnout %s + HTTP %d, zkontrolovat web v WebView + Odemknout %s + Přesunout sérii na konec + Relativní časová razítka + \"%1$s\" namísto \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-cv/strings.xml b/i18n/src/main/res/values-cv/strings.xml index bc9e4d457..f80d926e7 100644 --- a/i18n/src/main/res/values-cv/strings.xml +++ b/i18n/src/main/res/values-cv/strings.xml @@ -11,7 +11,7 @@ Юлашки вуланӑ сыпăк Сӳнтернӗ Вуланӑ хыҫҫӑн - Тиев вырӑнӗ + Тийев вырӑнӗ Вулани Кӑвак Симӗс @@ -27,7 +27,7 @@ Хура Шурӑ Ӳкерчĕк хыҫӑн тӗсӗ - Экран + Екран Хутлани Катерт Лартнӑ @@ -100,7 +100,6 @@ Пухмӑш хуш Хуш Умӗнхине вуланӑ пек паллӑ ту - Тухма тепӗр хут пус Улӑштар Пурне те сӳнтер Пурне те ҫут @@ -135,7 +134,7 @@ Wi-Fi ҫыхӑну ҫук Йӑнӑш Ҫӗнӗ версси пур! - Тиев йӑнӑшӗ + Тийев йӑнӑшӗ Тиенсе пӗтрӗ %1$s-мӗш сыпӑк Ҫӗнӗ сыпӑксем тупӑнман @@ -154,7 +153,6 @@ Меллӗ сетке Ҫӑтӑ сетке Тепӗр майлӑ суйла - Tachiyomi уҫ Сӳнтернӗ Сетке пысӑкӑшӗ Урлӑ @@ -220,11 +218,11 @@ Ҫаклатнӑ урлӑскер Ирӗклӗ Яланхилле урлӑ-тӑрӑх - Энимсӗр + Енимсӗр Пысӑклатни пуҫламӑш тӑрӑмӗ Тӑнлӑ Тӑс - Экранпа + Екранпа Сарӑмлани Вӗҫӗмсӗрех урлӑ Вӑрӑм елсем @@ -238,7 +236,7 @@ Куҫӑм Аланӑ сыпӑксене сиктермелле Вуланӑ сыпӑксене сиктермелле - Экрана ҫутӑ тыт + Екрана ҫутӑ тыт Тӗттӗмлетӳ Ҫутату Витӗм @@ -254,7 +252,7 @@ Ҫак хушмана урӑх кӗме май ҫук. Вӑл тӗрӗс мар ӗҫлеме тата апа йӑнӑшлаттарма пултарать. Ӑна катертме сӗнетпӗр. Ку хушмана шанчӑклӑ мар ӗнентерӳ хучӗпе алӑ пуснӑ тата ӑна пуҫарман. \n -\nСийенлӗ хушма Tachiyomi-ри упранакан кирек мӗнле шут пӗлӗмӗсене шута илме йе хӑй ирӗклӗ йума пурнӑҫлама пултарать. +\nСийенлӗ хушма упранакан кирек мӗнле шут пӗлӗмӗсене шута илме йе хӑй ирӗклӗ йума пурнӑҫлама пултарать. \n \nҪак ӗнентерӳ хутне шаннипе есӗ ҫав теветкеле йышӑнатӑн. @@ -279,15 +277,15 @@ Хушмасен ҫӗнетӗвӗсем Сыпӑксен ҫӗнетӗвӗсем Яланхи - Тиеве вӑхӑтлӑха чарнӑ + Тийеве вӑхӑтлӑха чарнӑ Кӗтмен йӑнӑша пула сыпӑксене тиесе илеймест - Тиевҫӗ + Тийевҫӗ Tachiyomi валли WebView кирлӗ Хушма валли ҫӗнетӳ пур %d хушма валли ҫӗнетӳ пур - Тиев… + Тийев… Ҫӗнетӳсем тупӑнман Тиесе ил Хуплашка суйла @@ -308,7 +306,7 @@ Ҫӗнӗ сыпӑксем %d хайлав валли тупӑннӑ Ҫӗнӗ сыпӑксен пуррине тӗрӗслени - Сыпӑксене тиесе илес ҫук. Тиевсем пайӗнче ҫӗнӗрен хӑтланса пӑхма пултаратӑн + Сыпӑксене тиесе илес ҫук. Тийевсем пайӗнче ҫӗнӗрен хӑтланса пӑхма пултаратӑн Ӑтавла Куҫар Куҫарма ҫӑл куҫ суйла @@ -349,7 +347,7 @@ Пӑрахнӑ Вӗҫленӗ Вулатӑп - Эсӗ суйланӑ сыпӑксене катертесшӗнех-и\? + Есӗ суйланӑ сыпӑксене катертесшӗнех-и\? Вуламанине Тиесе ил Тиенӗ вӑхӑчӗпе @@ -389,14 +387,13 @@ Юлашки усӑ курни Ыттисем Вырӑнти ҫӑл куҫ - Эле WebView-ра тӗрӗсле Пӗр тупсӑм та тупӑнман Урӑх тупсӑмсем ҫук Кантӑксем Тӗрӗс мар янтӑв файлӗ Пухмӑша ҫӗнетни Паллӑ мар йӑнӑш - Эсӗ тухрӑн + Есӗ тухрӑн Тух “%1$s тухмалла-и\? Ӑнӑҫлӑ кӗни @@ -567,7 +564,6 @@ Wi-Fi урлӑ ҫеҫ «Малалла вула» пускӑч Апа ҫаклатни, ыкран хӳтӗлевӗ - Петтерей тулли чух Чарусем: %s Вуламан сыпӑк(сем) пур Пуҫланӑ @@ -639,4 +635,7 @@ Малтанхилле Shizuku-н хушма ларткӑча усӑ курма Shizuku ларт тата ҫут. Йурӗ + %s уҫ + Серилӗхе вӗҫе куҫар + Кӗртнӗ пухмӑшсенче пулнӑ пулсан та кӑларса пӑрахнӑ пухмӑшсенче пулнӑ серилӗхсем ҫӗнелмӗҫ. \ No newline at end of file diff --git a/i18n/src/main/res/values-da/strings.xml b/i18n/src/main/res/values-da/strings.xml index 1aae251a9..ccd1e3144 100644 --- a/i18n/src/main/res/values-da/strings.xml +++ b/i18n/src/main/res/values-da/strings.xml @@ -7,7 +7,6 @@ Migerering Standard Advarsel - Lås op for Tachiyomi Bogmærket Fjern filter Inverter valg @@ -46,7 +45,6 @@ Intet læst for nyligt Ingen downloads Dato hentet - Tryk tilbage igen for at afslutte Indstillinger Ulæst Manga ialt @@ -180,7 +178,6 @@ Hver tredje dag Ugentligt Kun under opladning - Når batteriet ikke et lavt Begrænsninger: %s Spring over opdatering af titler Med ulæst kapitler diff --git a/i18n/src/main/res/values-de/strings.xml b/i18n/src/main/res/values-de/strings.xml index d054f8437..b0fa3de59 100644 --- a/i18n/src/main/res/values-de/strings.xml +++ b/i18n/src/main/res/values-de/strings.xml @@ -110,7 +110,7 @@ Links Rechts Mitte - Standard-Ausrichtungstyp + Standardausrichtung Frei Hochformat erzwingen Querformat erzwingen @@ -129,7 +129,7 @@ Ab viertletzt gelesenem Kapitel Ab fünftletzt gelesenem Kapitel Neue Kapitel herunterladen - Anbieter + Tracker Datensicherung erstellen Kann benutzt werden, um die aktuelle Bibliothek wiederherzustellen Datensicherung wiederherstellen @@ -249,7 +249,7 @@ Deinstallieren Diese Erweiterung ist mit einem nicht vertrauenswürdigen Zertifikat unterschrieben und wurde nicht aktiviert. \n -\nEine bösartige Erweiterung könnte in Tachiyomi gespeicherte Anmeldedaten auslesen oder einen schädlichen Code ausführen. +\nEine bösartige Erweiterung könnte gespeicherte Anmeldedaten auslesen oder einen schädlichen Code ausführen. \n \nDurch das Vertrauen dieses Zertifikats akzeptierst du diese Risiken. Keine Animation @@ -328,7 +328,6 @@ Fehler beim Umgehen von Cloudflare Bitte aktualisiere die WebView-App für eine bessere Kompatibilität Kapitelaktualisierungen - Tachiyomi entsperren Sicherer Bildschirm verbirgt App-Inhalte beim Wechseln von Apps und blockiert Screenshots Anzeige @@ -363,7 +362,6 @@ %d Erweiterungsaktualisierungen verfügbar Erweiterungsaktualisierungen - Webseite in WebView prüfen Bibliothek wird aktualisiert Gefilterte Kapitel überspringen Beim Lesen @@ -379,8 +377,7 @@ Mehr In der Bibliothek Zur Bibliothek hinzufügen - Zum Beenden nochmal die Zurück-Taste drücken - WebView ist für Tachiyomi erforderlich + WebView ist für das Funktionieren der App erforderlich Open-Source-Lizenzen Webseite Nur Heruntergeladenes @@ -412,9 +409,9 @@ Erledigt in %1$s mit %2$s Fehler Erledigt in %1$s mit %2$s Fehlern - Einweg-Synchronisation zum Aktualisieren der Kapitelfortschritte in den Trackingdiensten. Richte Tracking für einzelne Einträge über deren jeweiligen Tracking-Button ein. + Einweg-Synchronisation zum Aktualisieren der Kapitelfortschritte in den externen Trackingdiensten. Richte Tracking für einzelne Einträge über deren jeweiligen Trackingbutton ein. Bibliothekscover aktualisieren - Diese Erweiterung stammt nicht von der offiziellen Tachiyomi-Erweiterungsliste. + Diese Erweiterung stammt nicht aus der offiziellen Liste. Inoffiziell Nach Uploaddatum Daten @@ -451,7 +448,7 @@ Auf v%1$s aktualisiert Was ist neu Herunterladen von Kapiteln aufgrund von zu wenig Speicherplatz nicht möglich - Nach „%1$s“ überall suchen + Überall nach „%1$s“ suchen Hinzufügedatum Lesemodus Thema @@ -537,7 +534,7 @@ Hochformat Erstellt Ordner nach dem Titel der Einträge Speichere Seiten in separate Ordner - Ausrichtungstyp + Ausrichtung Aktionen Graustufen Inkognito-Modus deaktivieren @@ -575,8 +572,8 @@ Mitternachtsdämmerung Grüner Apfel App-Design - Erweiterte Anbieter - Anbieter, die für bestimmte Quellen erweiterte Funktionen anbieten. Einträge werden automatisch getrackt, wenn sie deiner Bibliothek hinzugefügt werden. + Erweiterte Tracker + Bieten für bestimmte Quellen erweiterte Funktionen an. Einträge werden automatisch getrackt, wenn sie deiner Bibliothek hinzugefügt werden. Dynamisch Hintergrundaktivität Niedrigste @@ -652,7 +649,6 @@ WebView-Daten löschen WebView-Daten gelöscht Schließen - Wenn der Akkustand nicht niedrig ist Keine installierte Quelle gefunden Keine Quelle gefunden Anzahl an Ungelesenem @@ -814,7 +810,14 @@ Bibliothekssynchronisierung abgeschlossen Tippe hier, um Hilfe zu Cloudflare zu erhalten Index der Downloads invalide - Tracking-Login + Tracker-Login Sicherungsdatei konnte nicht erstellt werden Lizenziert - Keine Kapitel zu zeigen + HTTP %d, überprüfe die Webseite in WebView + Keine Internetverbindung + %s konnte nicht erreicht werden + %s entsperren + Serie nach unten verschieben + Relative Zeitstempel + „%1$s“ anstelle von „%2$s“ \ No newline at end of file diff --git a/i18n/src/main/res/values-el/strings.xml b/i18n/src/main/res/values-el/strings.xml index 995b310b2..7439914a0 100644 --- a/i18n/src/main/res/values-el/strings.xml +++ b/i18n/src/main/res/values-el/strings.xml @@ -97,9 +97,9 @@ Μη αξιόπιστο Απεγκατάσταση Μη αξιόπιστη επέκταση - Αυτή η επέκταση υπογράφηκε με μη αξιόπιστο πιστοποιητικό και δεν ενεργοποιήθηκε. + Αυτή η επέκταση υπογράφηκε με αναξιόπιστο πιστοποιητικό και δεν ενεργοποιήθηκε. \n -\nΜια κακόβουλη επέκταση θα μπορούσε να διαβάσει τα διαπιστευτήρια σύνδεσης που είναι αποθηκευμένα στο Tachiyomi ή να εκτελέσει αυθαίρετο κώδικα. +\nΜια κακόβουλη επέκταση θα μπορούσε να διαβάσει τυχόν αποθηκευμένα διαπιστευτήρια σύνδεσης ή να εκτελέσει αυθαίρετο κώδικα. \n \nΕμπιστεύοντας αυτό το πιστοποιητικό αποδέχεστε αυτούς τους κινδύνους. Πλήρης οθόνη @@ -137,7 +137,7 @@ Χωρίς κίνηση Κανονική Γρήγορη - Προεπιλεγμένος τύπος περιστροφής + Προεπιλεγμένη περιστροφή Ελεύθερο Κλειδωμένο κατακόρυφα Κλειδωμένο οριζόντια @@ -156,7 +156,7 @@ Προ-προ-προτελευταίο αναγνωσμένο κεφάλαιο Προ-προ-προ-προτελευταίο αναγνωσμένο κεφάλαιο Λήψη νέων κεφαλαίων - Υπηρεσίες + Ιχνηλάτες Δημιουργία αντιγράφου ασφαλείας Μπορεί να χρησιμοποιηθεί για επαναφορά τρέχουσας βιβλιοθήκης Επαναφορά αντιγράφου ασφαλείας @@ -302,13 +302,12 @@ Χρησιμοποιήθηκε τελευταία Προσθήκη tracking Καρφιτσωμένα - Πατήστε ξανά για έξοδο %02d λεπτά, %02d δευτερόλεπτα Επιλογή αντίστροφου Ενημερώσεις επεκτάσεων Ενημερώσεις κεφαλαίων Παρακαλώ ενημερώστε την εφαρμογή WebView για καλύτερη συμβατότητα - Το WebView απαιτείται για το Tachiyomi + Το WebView απαιτείται για τη λειτουργία της εφαρμογής Κεφάλαιο %1$s και %2$d ακόμη Κεφάλαια %1$s Κεφάλαιο %1$s @@ -334,7 +333,6 @@ Διαθέσιμη ενημέρωση επέκτασης %d διαθέσιμες ενημερώσεις επεκτάσεων - Ελέγξτε τον ιστότοπο στο WebView Έχετε αποσυνδεθεί Αποσύνδεση Αποσύνδεση από %1$s; @@ -385,7 +383,6 @@ Προβολή κεφαλαίων Νεότερο κεφάλαιο Μενού - Ξεκλειδώστε το Tachiyomi Ληφθέντα μόνο Πηγές Παλαιότερο @@ -412,9 +409,9 @@ Έγινε σε %1$s με %2$s σφάλμα Έγινε σε %1$s με %2$s σφάλματα - Μονόδρομος συγχρονισμός για ενημέρωση των υπηρεσιών παρακολούθησης προόδου κεφαλαίων. Ρυθμίστε την παρακολούθηση για μεμονωμένες καταχωρήσεις από το κουμπί παρακολούθησης τους. + Μονόδρομος συγχρονισμός για ενημέρωση των υπηρεσιών εξωτερικής παρακολούθησης προόδου κεφαλαίων. Ρυθμίστε την παρακολούθηση για μεμονωμένες καταχωρήσεις από το κουμπί παρακολούθησης τους. Ανανέωση εξώφυλλων βιβλιοθήκης - Αυτή η επέκταση δεν προέρχεται από την επίσημη λίστα επεκτάσεων Tachiyomi. + Αυτή η επέκταση δεν προέρχεται από την επίσημη λίστα. Ανεπίσημη Από ημερομηνία μεταφόρτωσης Δεδομένα @@ -538,7 +535,7 @@ Δημιουργεί φακέλους σύμφωνα με τον τίτλο των καταχωρήσεων Αποθήκευση σελίδων σε ξεχωριστούς φακέλους Ενέργειες - Τύπος περιστροφής + Περιστροφή Κλίμακα του γκρι Απενεργοποίηση λειτουργίας ανώνυμης περιήγησης Αυτόματο @@ -575,8 +572,8 @@ Γιν και Γιανγκ Tako Φράουλα Daiquiri - Υπηρεσίες που παρέχουν βελτιωμένες δυνατότητες για συγκεκριμένες πηγές. Οι καταχωρήσεις παρακολουθούνται αυτόματα όταν προστίθενται στη βιβλιοθήκη σας. - Βελτιωμένες υπηρεσίες + Παρέχει βελτιωμένες δυνατότητες για συγκεκριμένες πηγές. Οι καταχωρήσεις παρακολουθούνται αυτόματα όταν προστίθενται στη βιβλιοθήκη σας. + Ενισχυμένοι trackers Δυναμικό Δραστηριότητα παρασκηνίου Χαμηλότερη @@ -643,7 +640,7 @@ Παραλήφθηκαν %1$d ενημέρωση(εις) παραλείφθηκε(-αν) Αντίστροφο πορτρέτο - Μετακίνηση σειράς στην κορυφή + Μετακίνηση σειράς προς τα πάνω Απενεργοποιημένο Μια νέα έκδοση είναι διαθέσιμη από τις επίσημες κυκλοφορίες. Πατήστε για να μάθετε πώς να μεταβείτε από ανεπίσημες κυκλοφορίες του F-Droid. Σφάλμα κατά την αποθήκευση της εικόνας @@ -652,7 +649,6 @@ Τα δεδομένα WebView διαγράφηκαν Διαγραφή δεδομένων WebView Κλείσιμο - Όταν η μπαταρία δεν είναι χαμηλή Δε βρέθηκε εγκατεστημένη πηγή Αριθμός μη αναγνωσμένων Τελευταίος έλεγχος ενημέρωσης @@ -817,4 +813,11 @@ Σύνδεση παρακολούθησης Δεν ήταν δυνατή η δημιουργία αντιγράφου ασφαλείας Αδειοδοτημένο - Δεν υπάρχουν κεφάλαια προς εμφάνιση + Δεν μπορούσε να φτάσει το %s + HTTP %d, ελέγξτε την ιστοσελίδα στο WebView + Δεν υπάρχει σύνδεση στο διαδίκτυο + Ξεκλείδωμα %s + Μετακίνηση σειράς προς τα κάτω + Σχετικές χρονικές σημάνσεις + \"%1$s\" αντί του \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-eo/strings.xml b/i18n/src/main/res/values-eo/strings.xml index 0d91f2c8a..857703896 100644 --- a/i18n/src/main/res/values-eo/strings.xml +++ b/i18n/src/main/res/values-eo/strings.xml @@ -47,7 +47,6 @@ Filtrilo Menuo Agordoj - Malŝlosi Tachiyomi-n Historio Ĉapitroj Biblioteka kontribuoj @@ -427,7 +426,6 @@ Nevalida ĉapitra formato Ĉapitro netrovita Loka fonta - Kontroli retejon en WebView Ĝisdatigi kategorion Kontroli por trovi ĝisdatigojn Helpu traduki @@ -449,7 +447,6 @@ Meznokta Vesperiĝo Ŝargas… Nuligi ĉiujn por ĉi tiu serio - Premi «reen» denove por eliri Aŭtentigi por konfirmi ŝanĝon Rezignita restaŭro Restaŭro fiaskis diff --git a/i18n/src/main/res/values-es/strings.xml b/i18n/src/main/res/values-es/strings.xml index 4c762aeb7..0f8cd0446 100644 --- a/i18n/src/main/res/values-es/strings.xml +++ b/i18n/src/main/res/values-es/strings.xml @@ -60,7 +60,7 @@ Cada 2 días Restricciones de actualización automática del dispositivo Mientras se carga la batería - Series completadas + Marcadas como completadas Actualizar progreso al terminar un capítulo Pantalla completa @@ -101,7 +101,7 @@ Ubicación personalizada - Servicios + Servicios de seguimiento Vaciar la caché de capítulos Usado: %1$s @@ -292,7 +292,7 @@ Copiar Esta extensión está firmada por una fuente sin certificar y por lo tanto no se ha activado. \n -\nUna extensión maliciosa puede leer credenciales de inicio guardadas en Tachiyomi o ejecutar cualquier tipo de código en tu dispositivo. +\nUna extensión maliciosa puede leer credenciales de inicio guardadas o ejecutar cualquier tipo de código en tu dispositivo. \n \nAl confiar en este certificado aceptas estos riesgos. Sin animación @@ -361,7 +361,6 @@ Fallo al evitar Cloudflare Actualiza la aplicación WebView para mejorar la compatibilidad Nuevos capítulos - Desbloquear Tachiyomi Pantalla segura oculta el contenido al cambiar de aplicación y bloquea las capturas de pantalla Visualización @@ -400,7 +399,6 @@ %d actualizaciones de extensiones disponibles Actualizando biblioteca - Abrir sitio en WebView Actualizaciones de extensiones Saltar capítulos excluidos por filtros Lectura @@ -414,12 +412,11 @@ Añadir seguimiento En biblioteca Añadir a biblioteca - Tachiyomi necesita WebView + Primero instala WebView para poder ver contenido de la aplicación Menos Más Licencias de código abierto Página web - Pulsa atrás de nuevo para salir Sólo ya descargados Caps. %1$s-%2$s Se ha cancelado la restauración @@ -454,7 +451,7 @@ Completada en %1$s con %2$s errores La sincronización de estos servicios solo funciona en un solo sentido. Cada elemento en tu biblioteca tiene un botón de seguimiento y tendrás que configurarlo a mano, uno a uno. - Esta extensión no es de la lista oficial de extensiones de Tachiyomi. + Esta extensión no es de la lista oficial de extensiones. No oficial Datos Fuentes que faltan: @@ -611,8 +608,8 @@ El formato del capítulo no es correcto No se ha encontrado el capítulo Desactivar el modo incógnito - Servicios que ofrecen funciones mejoradas para ciertas fuentes. Se hace un seguimiento automático de los elementos al añadirlos a la biblioteca. - Servicios mejorados + Ofrece funciones mejoradas para ciertas fuentes. Se hace un seguimiento automático de los elementos al añadirlos a la biblioteca. + Servicios de seguimiento mejorados Guía de seguimiento Automático No @@ -677,8 +674,8 @@ Desplazarse por el resto de la página antes de cambiar Cuadrícula sólo de portadas Acercar la vista en horizontal - Series sin empezar - Omitido porque la serie está completa + Sin empezar + Omitido, ya que su publicación ha terminado Omitido porque hay capítulos sin leer Omitido porque no hay capítulos leídos Ver más detalles @@ -695,7 +692,6 @@ Se han limpiado los datos del WebView Limpiar datos del WebView Cerrar - Cuando la batería tenga suficiente carga Todavía no se ha instalado ninguna fuente No se ha encontrado ninguna fuente Última comprobación de actualizaciones @@ -750,7 +746,7 @@ Populares Varios idiomas No se han concedido los permisos de almacenamiento - Saltando, porque este manga no necesita actualizarse + Omitido, ya que no es necesario actualizarse Buscar… Sincroniza tu progreso de lectura; unidireccional o mejorada Descargas automáticas y por adelantado @@ -861,7 +857,14 @@ Sincronizando la biblioteca Toca aquí para solucionar problemas de acceso con Cloudflare Se ha borrado el índice de descargas - Inicio de sesión de seguimiento + Iniciar sesión en el servicio No se ha podido crear un archivo de respaldo Con licencia oficial, sin capítulos que mostrar + No se ha podido acceder a %s + HTTP %d, comprueba la página web en WebView + Sin conexión a Internet + Desbloquear %s + Mover al último puesto + Marcas de tiempo relativas + «%1$s» en vez de «%2$s» \ No newline at end of file diff --git a/i18n/src/main/res/values-eu/strings.xml b/i18n/src/main/res/values-eu/strings.xml index 70776f91c..77162b700 100644 --- a/i18n/src/main/res/values-eu/strings.xml +++ b/i18n/src/main/res/values-eu/strings.xml @@ -443,7 +443,6 @@ Irakurri gabeko kapituluak ditu Sartu: %s G - Ikusi webgunea WebView-n Besteak Bilaketa globala… Historia ezabatu da @@ -483,9 +482,7 @@ Gehienezko babeskopiak Alfabetikoki Oharra - Desblokeatu Tachiyomi Autentifikatu aldaketa berresteko - Sakatu atzera berriro irteteko Ezarpenak Menua Iragazi @@ -640,7 +637,6 @@ Ezabatu kategoria Itxi App hizkuntza - Bateria baxu ez dagoenean Hizkuntza Bilatu… \"%s\" kategoria ezabatu nahi duzu\? diff --git a/i18n/src/main/res/values-fa/strings.xml b/i18n/src/main/res/values-fa/strings.xml index 3606fbea1..cb38c0439 100644 --- a/i18n/src/main/res/values-fa/strings.xml +++ b/i18n/src/main/res/values-fa/strings.xml @@ -97,7 +97,6 @@ Dodge / Lighten هر ۱۲ ساعت یکبار انتخاب دسته بندی - بازکردن تاچی یومی هیچ دسته بندی وجود ندارد. با زدن دکمه به علاوه دسته بندی جدیدی بسازید. کتابخانه شما خالی است هیچ چیز مانگاای به تازگی خوانده شده @@ -128,7 +127,6 @@ آخرین استفاده دیگر منبع محلی - وب سایت را در WebView بررسی کنید هیچ نتیجه ای یافت نشد نتیجه بیشتری یافت نشد تب ها @@ -428,7 +426,6 @@ فیلتر منو تنضیمات - برای بستن دکمه بازگشت را یک بار دیگر بزنید تاریخچه قسمت ها کتابخانه ورودی‌ها @@ -559,7 +556,6 @@ با قسمت‌(های) خوانده‌نشده ردیاب‌ها را به طور خودکار تازه کن - هنگامی که باتری کم نیست از به‌روزرسانی ورودی‌ها صرف‌نظر کنید امروز هر 3 روز diff --git a/i18n/src/main/res/values-fi/strings.xml b/i18n/src/main/res/values-fi/strings.xml index bd4a39a51..c97467e5f 100644 --- a/i18n/src/main/res/values-fi/strings.xml +++ b/i18n/src/main/res/values-fi/strings.xml @@ -323,7 +323,6 @@ Cloudflaren ohittaminen epäonnistui Lukujen päivitykset - Avaa Tachiyomi Näyttö Päivitettävät Päivitä WebView-sovellus yhteensopivuuden parantamiseksi @@ -367,7 +366,6 @@ Varmuuskopion palautus epäonnistui Varmuuskopiointi epäonnistui Ohita suodatetut luvut - Paina takaisin uudestaan poistuaksesi Ladatut Lähteet Päivittää kirjastoa @@ -388,7 +386,6 @@ Vähemmän Enemmän Kiinnitetyt - Katso sivu WebViewissä Luettavana Tarkista päivitykset Viimeksi käytetty @@ -621,7 +618,6 @@ Korkein Kieli Haku… - Kun akku ei ole vähissä Laventeli Sovelluksen kieli Versio diff --git a/i18n/src/main/res/values-fil/strings.xml b/i18n/src/main/res/values-fil/strings.xml index e9698d55e..bc086ddf6 100644 --- a/i18n/src/main/res/values-fil/strings.xml +++ b/i18n/src/main/res/values-fil/strings.xml @@ -69,8 +69,6 @@ Pansala Menu Mga setting - Pindutin muli para umalis - Buksan ang Tachiyomi Nakaraan Tina-track Mga Kabanata @@ -124,7 +122,7 @@ Pinili kong lugar Pagkamarkahang nabasa na Pagkatapos basahin, kusang burahin - Lugar ng paglalagyan + Lokasyon sa pag-download Kapal ng gilid Pagbabasa Ipakita palagi ang paglipat-kabanata @@ -187,11 +185,11 @@ I-animate ang paglipat-pahina Ipakita ang laman sa cutout area Naka-fullscreen - Wala sa opisyal na listahan ng mga extension ng Tachiyomi ang extension na ito. + 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. \n -\nMaaaring mabasa ng isang kaduda-dudang extension ang kahit anong credentials sa pag-login na nakatago sa Tachiyomi o di kaya nama\'y magsimula ng delikadong code. +\nMaaaring mabasa ng isang kaduda-dudang extension ang kahit anong nakatagong credentials sa pag-login o di kaya nama\'y magsimula ng delikadong code. \n \nTinatanggap mo ang mga bantang ito sa pagtiwala sa certificate na ito. Kaduda-dudang extension @@ -271,7 +269,7 @@ Nagka-error Taga-download Paki-update po ang WebView app para sa mas maayos na paggana - Kailangan ng Tachiyomi ang WebView + Kinakailangan ng app ang WebView upang gumana ito Bigong ma-bypass ang Cloudflare Ang extension ay available upang i-update @@ -392,7 +390,6 @@ Huling ginamit Iba pa Lokal na Source - Bisitahin ang site sa WebView Walang nakitang resulta Wala na\'ng resulta Mga Tab @@ -652,7 +649,6 @@ Nalinis na ang WebView data Linisin ang WebView data Isara - Kapag di lowbat Walang nakitang naka-install na source Walang nakitang source Huling pag-update @@ -698,7 +694,7 @@ Sigurado ka ba\? Susunod na hindi pa nababasa na kabanata - Susunod na ang %d na hindi pa nababasa na kabanata + Susunod na %d di pa nababasa na kabanata Marami Tatanggalin mo na ang \"%s\" mula sa aklatan mo @@ -802,7 +798,7 @@ Itakdang i-update bawat Sa labas ng inaasahang release period Mga pagitan - I-customize ang Interval + Ayusin ang interval Nilaktawan dahil walang inaasahang release ngayong araw May mga resulta Tanggalin ang na-download @@ -817,4 +813,11 @@ Hindi makalikha ng backup file Pag-login sa tracking Lisensyado - Walang mapakitang kabanata + I-unlock ang %s + Walang koneksyon sa Internet + HTTP %d, tignan ang website sa WebView + Hindi maabot ang %s + Ilagay sa ibaba ang serye + Mga relatibong timestamp + \"%1$s\" sa halip na \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-fr/strings.xml b/i18n/src/main/res/values-fr/strings.xml index ccc7c76da..b6c90e684 100644 --- a/i18n/src/main/res/values-fr/strings.xml +++ b/i18n/src/main/res/values-fr/strings.xml @@ -104,7 +104,7 @@ Gauche Droite Centre - Type de rotation par défaut + Rotation par défaut Libre Bloqué sur portrait Bloqué sur paysage @@ -282,7 +282,7 @@ Extension non reconnue Cette extension a été signée avec un certificat non fiable et n\'a pas été activée. \n -\nUne extension malveillante pourrait lire n\'importe quel identifiant de connexion stocké dans Tachiyomi ou exécuter un code arbitraire. +\nUne extension malveillante pourrait lire n\'importe quel identifiant de connexion stocké ou exécuter un code arbitraire. \n \nEn faisant confiance à ce certificat, vous acceptez ces risques. Vitesse d\'animation du double-clic @@ -313,7 +313,7 @@ Échec du chargement des pages : %1$s Déplacer Copier - Afficher en cas d\'appui long + Afficher les actions en appuyant longuement Ouvrir dans WebView Couleurs à 32 bits Passer les chapitres marqués comme lus @@ -363,7 +363,6 @@ Impossible de contourner Cloudflare Veuillez mettre à jour l\'application WebView pour une meilleure compatibilité Mises à jour des chapitres - Débloquer Tachiyomi L\'écran sécurisé cache le contenu lors du changement d\'application et bloque les captures d\'écran Affichage @@ -402,7 +401,6 @@ %d mises à jour d\'extensions disponibles %d mises à jour d\'extensions disponibles - Consultez le site Web de WebView Mise à jour de la bibliothèque Sources En cours @@ -427,11 +425,10 @@ Site web Ch. %1$s – %2$s Licences à code source ouvert - Appuyer à nouveau pour quitter Dernière utilisée Rechercher des mises à jour %02d min, %02d s - WebView est requis par Tachiyomi + WebView est requis pour le fonctionnement l\'application Guide des sources locales %1$s restant @@ -456,7 +453,7 @@ Impossible d\'ouvrir les paramètres de l\'appareil Actualiser les couvertures de la bibliothèque Synchronisation à sens unique pour mettre à jour la progression du chapitre dans les services de suivi. Configurez le suivi des entrées individuelles à partir de leur bouton de suivi. - Cette extension ne fait pas partie de la liste des extensions officielles de Tachiyomi. + Cette extension ne fait pas partie de la liste officielle. Non officiel Vérifier s\'il y a une nouvelle couverture ou synopsis lors des mises à jour de la bibliothèque Par date de téléversement @@ -592,8 +589,8 @@ Trier par Format de chapitre invalide Chapitre non trouvé - Mode de rotation - Automatique + Rotation + Auto Mettre à jour les services de suivi lors de la mise à jour de la bibliothèque Actualiser automatiquement les services de suivi Restrictions : %s @@ -606,7 +603,7 @@ Manuel de suivi Désactivé Activé - Rendre les réglages de tri et d\'affichage propres à chaque catégorie + Rendre les réglages de tri propres à chaque catégorie Vous n\'avez pas encore de catégories. Commencer à télécharger Mise à jour de la bibliothèque… (%1$d/%2$d) @@ -663,7 +660,7 @@ Attention : les téléchargements massifs peuvent entraîner un ralentissement des sources ou le blocage de Tachiyomi. Appuyez pour en savoir plus. Tout mettre à jour Mises à jour de l\'application - Effacer le cache du chapitre à la fermeture de l\'application + Vider le cache de chapitre au lancement de l\'application %1$d entrées qui ne sont pas dans la bibliothèque dans la base de données Rien à effacer Échec de la récupération de la liste des extensions @@ -679,7 +676,7 @@ Afficher le titre Grille avec seulement la couverture Panoramique des images larges - Agrandir les images en paysage + Zoom automatique dans les images larges Qui n\'ont pas encore commencé Sauté car la série est terminée Sauté car aucun chapitre n\'est lu @@ -698,7 +695,6 @@ Effacer les données WebView Données WebView effacées Fermer - Quand la batterie n\'est pas faible Aucune source installée trouvée Dernière verification de mise à jour Nombre de non-lus @@ -745,7 +741,7 @@ Les %d suivants non lus Les %d suivants non lus - Fonctionne seulement sur les entrées de la bibliothèque et si le chapitre actuel et le suivant sont déjà téléchargés + Fonctionne seulement si le chapitre actuel et le suivant sont déjà téléchargés. Êtes-vous sûr(e) \? Populaire Diviser les grandes images (BETA) @@ -754,7 +750,7 @@ Vous êtes sur le point de retirer « %s » de votre bibliothèque Autorisations de stockage non accordées Recherche… - Passées car la série ne nécessite pas de mise à jour + Ignorée car la série ne nécessite pas de mises à jour Oups ! Thème, format de la date et de l\'heure Téléchargement automatique, téléchargement anticipé @@ -838,11 +834,33 @@ Appuyez deux fois pour zoomer Recherche mensuelle (28 jours) Vérification tardive 10+ - Abandonné \? Fin 20+ et 2 mois + 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 Définir l\'intervalle Valider Intervalle de recherche personnalisé + Appuyez ici pour de l\'aide sur Cloudflare + Débloquer %s + Synchronisation de la bibliothèque + Intervalles + Synchronisation de la bibliothèque complété + Licenciés - Aucun chapitres à montrer + Aucune connexion internet + Indice de téléchargement invalidé + Ignoré car aucune sortie n\'était attendue aujourd\'hui + A des résultats + Estimer chaque + HTTP %d, consulter le site Web dans WebView + + 1 jour + %d jours + %d jours + + Configurer pour mettre à jour tous les + Personnaliser l\'intervalle + Déplacer la série vers le bas + Supprimez également de %s + Impossible de joindre %s \ No newline at end of file diff --git a/i18n/src/main/res/values-gl/strings.xml b/i18n/src/main/res/values-gl/strings.xml index 5aa5f65b2..1b58a9ac9 100644 --- a/i18n/src/main/res/values-gl/strings.xml +++ b/i18n/src/main/res/values-gl/strings.xml @@ -230,8 +230,6 @@ Filtro Menú Axustes - Preme atrás novamente para saír - Desbloquea Tachiyomi Historial Seguimento Capítulos @@ -368,7 +366,6 @@ Baixados Idioma da aplicación ErroInterno: Revisa o rexistro de erros para máis información - Cando a batería non estea baixa Información da aplicación Shizuku non se está executando Instala e inicia Shizuku para utilizalo como instalador de extensións. @@ -720,7 +717,6 @@ Páxina anterior Lista de lectura Estás seguro\? - Abrir o sitio web no WebView Outros Estado descoñecido Fonte non instalada: %1$s diff --git a/i18n/src/main/res/values-he/strings.xml b/i18n/src/main/res/values-he/strings.xml index 0c255f305..d419e9157 100644 --- a/i18n/src/main/res/values-he/strings.xml +++ b/i18n/src/main/res/values-he/strings.xml @@ -3,7 +3,7 @@ עדכונים לתוסף עדכונים לפרק נפוץ - ההורדה מושהית + ההורדות מושהות אין חיבור רשת זמין אין חיבור Wi-Fi זמין לא ניתן להוריד את הפרק בגלל שגיאה בלתי צפויה @@ -21,13 +21,13 @@ גרסה חדשה זמינה! שגיאה בהורדה - לחץ כדי להתקין + לחץ כדי להתקין עדכון מוריד… אין עדכונים חדשים זמינים הורדה בחר קובץ גיבוי בחר תמונת כריכה - אנא הוסף את המנגה לספרייה שלך לפני שתעשה זאת + אנא הוסף את הפריט לספרייה שלך לפני שתעשה זאת עדכון הכריכה נכשל פרקים %1$s ואחד נוסף @@ -74,7 +74,7 @@ הכריכה עודכנה סנן מותאם אישית התמונה נשמרה - אפס את כל הפרקים למאנגה זו + אפס את כל הפרקים לפריט זה פעולה זו תסיר את התאריך הקריאה של פרק זה. האם אתה בטוח\? הקטגוריות נמחקו קטגוריה עם שם זה כבר קיימת! @@ -99,7 +99,7 @@ שגיאה מוריד (%1$d/%2$d) פרקים %1$s - להוסיף מאנגה לספרייה\? + להוסיף לספרייה\? המקור לא הותקן: %1$s למחוק פרקים שירדו\? נוסף לספרייה @@ -111,7 +111,6 @@ חיפוש גלובלי… אחר מאנגה מקומית - בדוק אתר ב-WebView לא נמצאו תוצאות אין תוצאות נוספות מעדכן קטגוריה @@ -167,8 +166,8 @@ לעולם לא תמיד נעילה כאשר אינו פעיל - דורש ביטול נעילה - אבטחה + ביטול נעילה דרוש + אבטחה ופרטיות ניהול התראות תבנית תאריך פעיל @@ -224,8 +223,8 @@ השבתת האופטימיזציה של הסוללה עדכן סטטוס, ציון ופרק אחרון שנקרא משירותי המעקב הרשומות נמחקו - האם אתה בטוח\? פרקים שנקראו וההתקדמות של מנגה שאינה בספרייה יאבדו - מחק את היסטוריית המנגה שאינם שמורים בספריה שלך + האם אתה בטוח\? פרקים שנקראו וההתקדמות של פריטים שאינם בספרייה יאבדו + מחק את היסטוריית הפריטים שאינם שמורים בספריה שלך נקה את מסד הנתונים עוגיות נוקו נקה עוגיות @@ -282,10 +281,10 @@ למתוח התאם לגודל מסך סוג קנה מידה - Webtoon - אנכי - ימין לשמאל - שמאל לימין + רצועה מוארכת + סידור עמודים (אנכי) + סידור עמודים (ימין לשמאל) + סידור עמודים (שמאל לימין) ברירת המחדל של מצב הקריאה שחור לבן @@ -307,7 +306,7 @@ הנפשת מעברי דפים הצג תוכן באזור החתוך תוסף לא מאומת - תוסף זה אינו זמין עוד. + תוסף זה אינו זמין עוד. ייתכן שאינו פועל כצפוי ויכול לגרום לבעיות באפליקציה. מומלץ להסיר את התקנת התוסף. מסך מלא הוסף ערוך @@ -344,12 +343,10 @@ הגדרות היסטוריה פרקים - מנגה + פריטים בספריה קטגוריות שם - לחץ אחורה שוב כדי לסגור - שחרר את Tachiyomi - שום דבר נקרא לאחרונה + שום דבר לא נקרא לאחרונה אין עידכונים אחרונים סומן רענון עוקבים אוטומטית @@ -358,7 +355,7 @@ התחל להוריד עכשיו נעץ מסך מאובטח - פריטים לשורה + גודל רשת עקוב אחר המערכת בדוק אם יש כריכה ופרטים חדשים בעת עדכון הספרייה סה\"כ פריטים @@ -406,7 +403,7 @@ תאריך הוספה תאריך האחזור של הפרקים דינמי - מנגה הנמצאת בקטגוריית מנועי העדכונים לא תעודכן גם אם היא נכללת בקטגורייה אחרת שכן מתעדכנת. + פריט הנמצא בקטגוריית מנועי העדכונים לא יעודכן גם אם הוא נכלל בקטגורייה אחרת שכן מתעדכנת. הראה פריט עם פרק(ים) שלא נקרא(ו) העבר סדרה לראש @@ -428,7 +425,7 @@ הראה מצב קריאה בלתי רשמי פרטי האפליקצייה - דלג על עדכוני כותרות + דלג על עדכוני פריטים נכשל בקבלת רשימת ההרחבות שו\"ת ומדריכים @@ -462,7 +459,7 @@ הכי נמוך מחק פרקים קטגוריות מוחרגות - מנגה הנמצאת בקטגוריית מנועי ההורדות לא תעודכן גם אם היא נכללת בקטגוריה אחרת שכן נכללת (בהורדות). + פריט הנמצא בקטגוריית מנועי ההורדות לא יעודכן גם אם הוא נכלל בקטגוריה אחרת שכן נכללת (בהורדות). שמור כארכיון CBZ שירותים משופרים גבול @@ -473,25 +470,25 @@ מצב קריאה הורדה אוטומטית אפור - אנכי מתמשך - עמודים + רצועה מוארכת עם רווחים + סידור עמודים מאונך הפוך שמור דפים בתיקיות נפרדות אנכי שניהם פעולות - הראה בלחיצה ארוכה + הצג פעולות בלחיצה ארוכה אוטומטי אפשר מחיקת פרקים שסומנו מנוע - צור תיקיות בהתאם לכותרת המנגה - שירותים המספקים שירותים משופרים למקורות ספציפיים. מנגות יהיו במעקב אוטומטי אחרי הוספה לספרייה שלך. + צור תיקיות בהתאם לכותרת הפריטים + שירותים המספקים שירותים משופרים למקורות ספציפיים. פריטים יהיו במעקב אוטומטי אחרי הוספה לספרייה שלך. מקורות חסרים: קובץ גיבוי לא תקין - הגיבוי לא מכיל שום מנגה. + הגיבוי לא מכיל שום פריטים. מעקב - נקה את זיכרון המטמון של הפרקים כשהאפליקציה נסגרת - יש %1$d מנגה שנמצאות במסד הנתונים אבל לא בספרייה + נקה את זיכרון המטמון של הפרקים כשהאפליקציה עולה + יש %1$d פריטים שנמצאים במסד הנתונים אבל לא בספרייה לשוניות חפש את \"%1$s\" גלובלית מצב לא ידוע @@ -510,7 +507,7 @@ שגיאה בשיתוף הכריכה קבע כברירת המחדל מצב קריאה - מנגה מתוך הספרייה + מהספרייה פרקים שירדו כריכה יותר @@ -544,20 +541,20 @@ 1 נשאר 2 נשארו %1$s נשארו - %1$s נשארו + התחברות נעוץ %02d דקות, %02d שניות השחזור בוטל - עדכונים אוטומטיים מאוד מומלצים. רצוי לשמור עותקים נוספים במקומות אחרים. - מסד הנתונים נקי + רצוי לשמור עותקים נוספים של גיבויים במקומות אחרים בנוסף. + אין מה לנקות בדוק עדכונים סדר ע\"י תאריך מחבר לא ידוע מצב פרטי - סינון כל המנגה בספרייה שלך + סינון כל הפריטים בספרייה שלך אתר מידע שגיאה בשמירת התמונה @@ -569,7 +566,7 @@ הושלם ב %1$s עם שגיאה אחת הושלם ב %1$s עם שתי שגיאות הושלם ב %1$s עם %2$s שגיאות - הושלם ב %1$s עם %2$s שגיאות + רשת בוטל/ה @@ -598,8 +595,7 @@ נכשל איפוס הגדרות מצב הקריאה עדכון אחרון לא נקראו - אין קצת סוללה - הגדרות מיון ותצוגה לכל קטגוריה בנפרד + הגדרות מיון לכל קטגוריה בנפרד מקור לא נמצא %1$d עדכונים דולגו גרסת האנדרואיד הזאת כבר לא נתמכת @@ -607,8 +603,8 @@ רשת רק של הכריכות פתח את יומן האירועים %1$d עדכונים נכשלו - הגדל תמונה אופקית - משפר את ביצועי מצב הקריאה ע\"י חיתוך תמונות גבוהות שירדו. + בצע קירוב לתמונות אופקיות באופן אוטומטי + משפר את ביצועי מצב הקריאה דולג בגלל שהסדרה נגמרה דולג בגלל שיש פרקים שלא נקראו דולג בגלל שאין פרקים שנקראו @@ -618,7 +614,7 @@ דולג פרק אחד, המקור חסר או שהוא סונן החוצה דולגו שני פרקים, המקור חסר או שהם סוננו החוצה דולגו %d פרקים, המקור חסר או שהם סוננו החוצה - דולגו %d פרקים, המקור חסר או שהם סוננו החוצה + מפחית פסים, אך עשוי להשפיע על הביצועים שגיאות @@ -634,7 +630,7 @@ פורמט פרק לא תקין בהפסקה האם תרצה למחוק את הקטגוריה %s\? - פיצול לשני עמודים + פצל עמודים רחבים כמו ספר אלקטרוני גרסה כיסוי @@ -646,10 +642,10 @@ הגבלה מורשת שפת אפליקציה - היפוך שני העמודים + הפוך מיקום של עמודים מפוצלים מדריך מעקב מחק הכל - אם המקום של הפיצול עמוד לא תואם לכיוון הקריאה + אם המקום של העמוד המפוצל לא תואם לכיוון הקריאה כלום איזורי נגיעה הפורמט RARv5 לא נתמך @@ -658,4 +654,198 @@ מדריך למתחיל סטטיסטיקות מקומי + *דרוש + שגיאה פנימית: בדוק ביומני קריסה למידע נוסף + נעילת אפליקציה, מסך מוגן + אתה עומד למחוק את \"%s\" מהספריה שלך + הותחלו + הורדו + גלים + קבע מרווח זמן + קבע מרווח זמן מותאם מראש לטעינה + טען חודשית (28 ימים) + כותרת לא מוכרת + בחר מרווח זמן מותאם אישית + מנהל ההורדות + HTTP %d, בדוק באתר ב-WebView + אין חיבור אינטרנט + קטגוריה ריקה + קטגוריית עדכון + כפתור המשך קריאה + מקורות, הרחבות, חיפוש כללי + מצב קריאה, תצוגה, ניווט + העתק ללוח הכתיבה + הגדרות פרק דיפולטי עודכנו + לא עכשיו + בודק הורדות + מחק הורדות + בדוק 10+ באיחור + נפל\? באיחור 20+ וחודשיים + תקופת בדיקה עברה + העדכון הצפוי הבא + הורד קבצי קריסה, אופטימיזציה לסוללה + לבנדר + %d בכל שורה + + הפרק הבא + שני הפרקים הבאים + %d הפרקים הבאים + + + לחץ כאן לעזרה עם Cloudflare + עמוד %d לא נמצא בעת פיצול + צפה בפריטי הספריה העדכניים ביותר + נכשל במציאת כתובת עמוד %d + כבר קיים פריט בספריה בעל אותו שם. +\n +\nלהמשיך בכל זאת\? + הורדה אוטומטית, הורד את הבאים + נושא, פורמט תאריך וזמן + קטגוריות, עדכון כללי, החלקת פרק + סנכרון חד צדדי, סנכרון משופר + מדריך וגיבויים אוטומטיים + לא נמצאו פריטים בקטגוריה זו + בסדר + + מעקב אחד + שני מעקבים + %d מעקבים + + + ספריה עודכנה לאחרונה ב: %s + פצל תמונות מוארכות (בטא) + שטח תמונה רחבה + הסתר פריטים שכבר בספרייה + הרשאות אחסון לא ניתנו + אין פרטי ספרייה לגיבוי + נכשל ביצירת קובץ גיבוי + מחרוזת משתמש לא תקינה + איפוס מחרוזת משתמש דיפולטית + לא הצליח לפתוח הגדרות מכשיר + לוג מפורט + הדפס דוחות שגיאה מפורטים ליומן המערכת (מוריד ביצועים לאפליקציה) + שכבת על + %d ימים + פתח ב-GitHub + פעולה בהחלקה ימינה + בטל תוקף של אינדקס ההורדות + זמין אבל המקור לא מותקן: %s + סנכרון חד צדדי לעדכון ההתקדמות בפרקים בשיקות המעקב. קבע מעקב עבור פריטים ספציפיים מכפתור המעקב שלהם. + הצג כמות פרקים שלא נקראו בסמל העדכונים + פצל תמונה מוארכת + שומר דוח שגיאות לקובץ עבור שיתוף עם המפתחים + כריכה מותאמת אישית + להסיר תאריך\? + הורד הלאה + פעילות רקע + פריטים במעקב + מחרוזת משתמש דיפולטית + מעבר פרק + בעל רישיון - אין פרקים להראות + הועתק ללוח כתיבה + האם אתה בטוח\? + המקור לא נתמך + לא נמצאה התאמה + לא מותקן + משך קריאה + בעדכון כללי + N/A + לזערה לגבי איך לתקן בעיות בעדכוני ספריה, ראה %1$s + לא יכל להשיג את %s + מחוץ לתקופת הפרסום הצפויה + סובב עמודים רחבים להתאמה + ממש עכשיו + הורד אוטומטית בזמן קריאה + פרטי התחברות למעקב + מיקום לא תקין: %s + מסנכרן ספריה + מחרזות משתמש לא יכולה להיות ריקה + שומש לאחרונה + בבקשה התחבר ל-MAL שוב + פריטים שהושלמו + %s נתקל בשגיאה בלתי צפויה. אנו ממליצים שתשתף את דוחות השגיאה שלך בערוץ התמיכה שלנו בדיסקורד. + נקראו + פריטים + %d שניות + רשימת פריטים בעצירה זמנית + רשימת פריטים שלא הושלמו + פעולת עדכון כבר פועלת + מעקבים + החלף אוריינטציה של עמודים רחבים מסובבים + מעקבים שדורשים התחברות: + רשימת פריטים שהושלמו + ממשק משתמש טאבלט + F-Droid builds לא נתמכים באופן רשמי. +\nלחץ כדי ללמוד עוד. + להסיר את %s ממעקב\? + פעולה זו תסיר את המעקב שלך מקומית. + הסר גם מ-%s + קבע גם עבור כל הפריטים בספריה + רק ברשת בלתי מוגבלת + שתף יומני קריסה + רענן מעקבים + פעולה זו תסיר את תאריך ההתחלה הקודם שלך מ-%s + רשימת קריאה + רשימת תכנונים + רבים + להתחמק / להאיר + רפד בצדדים + סנכרון ספריה הושלם + מרווחי זמן + לא מסוגל לפתוח את הפרק שנקרא לאחרונה + פעולה בהחלקה שמאלה + לחיצה כפולה לקירוב (זום) + עודכן לגרסה %1$s + בסך הכל + שגיאה %1$s: %2$s + עובד רק אם הפרק הנוכחי והפרק הבא כבר הורדו. + דלג על פרקים כפולים + DNS over HTTPS (DoH) + תוקף אינדקס ההורדות בוטל + מידע של WebView נוקה בהצלחה + אפס מצב קריאה ואוריינטציה לכל הסדרות + + הפרק הבא שלא נקרא + שני הפרקים הבאים שלא נקראו + %d הפרקים הבאים שלא נקראו + + + מידע דיבוג + רישיונות מקורות פתוחים + פופולרי + + יום אחד + יומיים + %d ימים + + + + חסר פרק אחד + חסרים שני פרקים + חסרים %1$s פרקים + + + הוסף מעקב + לחלק מהיצרנים יש הגבלות אפליקציה שהורגות תהליכים ברקע. באתר הזה יש עוד מידע לגבי איך לתקן את זה. + פעולה זו תסיר את תאריך הסיום הקודם שלך מ-%s + אופס! + אתחל את האפליקציה + %d שעות + דולג בגלל שהספריה לא דורשת עדכונים + דולג בגלל שלא צפוי פרסום היום + לא נמצאה אפליקציה לאסוף קבצים + במעקב + אין תיאור + בצע אומדן בכל + קבע לעדכון בכל + פרקים %1$s-%2$s + סקירה + יישומון לא זמין כשנעילת האפליקציה מופעלת + הכרח את האפליקציה לבדוק מחדש פרקים שהורדו + נקה מידע של WebView + רענן כריכות ספרייה + בעל תוצאות + ציון ממוצע + בשימוש + %d דקות \ No newline at end of file diff --git a/i18n/src/main/res/values-hi/strings.xml b/i18n/src/main/res/values-hi/strings.xml index c8e28abc6..d4cff453b 100644 --- a/i18n/src/main/res/values-hi/strings.xml +++ b/i18n/src/main/res/values-hi/strings.xml @@ -328,7 +328,6 @@ Cloudflare को बायपास करने में विफल बेहतर संगतता के लिए कृपया WebView ऐप को अपडेट करें अध्याय अद्यतन - अनलॉक Tachiyomi सुरक्षित स्क्रीन एप्लिकेशन स्विच करते समय एप्लिकेशन सामग्री छिपाता हैं और स्क्रीनशॉट ब्लॉक करता हैं प्रदर्शन @@ -363,7 +362,6 @@ %d एक्सटेंशन अपडेट उपलब्ध एक्सटेंशन अपडेट - WebView में वेबसाइट देखें अद्यतन पुस्तकालय पठन फ़िल्टर किए गए अध्यायों को छोड़ें @@ -382,7 +380,6 @@ ताचियोमी के लिए WebView आवश्यक है ओपन सोर्स लाइसेंस वेबसाइट - बाहर निकलने के लिए फिर से वापस दबाएं केवल डाउनलोड किए गए अध्याय %1$s – %2$s बैकअप बहाल करने में विफल रहा @@ -649,7 +646,6 @@ कोई स्थापित स्रोत नहीं मिला आखिरी आइटम अद्यतन अपठित गिनती - जब बैटरी कम नहीं WebView डेटा साफ हो गया गिटहब में खोलें चित्र सहेजने में त्रुटि diff --git a/i18n/src/main/res/values-hr/strings.xml b/i18n/src/main/res/values-hr/strings.xml index 1dc7fa125..d7416aafe 100644 --- a/i18n/src/main/res/values-hr/strings.xml +++ b/i18n/src/main/res/values-hr/strings.xml @@ -11,7 +11,7 @@ Ovo proširenje više nije dostupno. Možda neće ispravno funkcionirati i može uzrokovati probleme s aplikacijom. Preporučuje se deinstalacija. Ovo proširenje potpisano je nepovjerljivim certifikatom i nije aktivirano. \n -\nZlonamjerno proširenje može pročitati sve podatke za prijavu koji su spremljeni u programu Tachiyomi ili izvršiti proizvoljni kod. +\nZlonamjerno proširenje može pročitati sve spremljene podatke za prijavu ili izvršiti proizvoljni kod. \n \nVjerujući ovom certifikatu, prihvaćaš te rizike. Nepovjerljivo proširenje @@ -152,8 +152,6 @@ Filtar Izbornik Postavke - Za zatvaranje programa ponovo pritisni natrag - Otključaj Tachiyomi Povijest Praćenje Unosi u biblioteci @@ -168,7 +166,7 @@ Obnovi sigurnosnu kopiju Može se koristiti za obnavljanje trenutačne biblioteke Stvori sigurnosnu kopiju - Usluge + Usluge praćenja Ažuriraj napredak nakon čitanja Preuzmi nova poglavlja Peto prije zadnjeg pročitanog poglavlja @@ -281,7 +279,7 @@ Trenutačno: Završeno: Modus čitanja - Za ovaj serijal + Za ovu seriju Ovu sliku koristiti kao naslovnicu\? Nije bilo moguće učitati sliku Sljedeće poglavlje nije pronađeno @@ -342,7 +340,6 @@ Zadnji korišteni Drugi Lokalni izvor - Provjeri web-stranice pomoću WebView Nema rezultata Nema daljnjih rezultata Ažuriranje kategorije @@ -399,7 +396,7 @@ Dostupne su %d nove verzije proširenja Dostupno je %d novih verzija proširenja - Za Tachiyomi je potreban WebView + Za funkcioniranje programa je potreban WebView %1$d novo poglavlje %1$d nova poglavlja @@ -426,8 +423,8 @@ Nedostaju izvori: Sigurnosna kopija ne sadrži unose u biblioteci. Neispravna datoteka sigurnosne kopije - Jednosmjerna sinkronizacija za aktualiziranje napretka poglavlja u usluzi praćenja. Postavi praćenje pojedinačnih unosa manga putem gumba za praćenje. - Ovo proširenje nije sa službenog popisa Tachiyomi proširenja. + Jednosmjerna sinkronizacija za aktualiziranje napretka poglavlja u eksternoj usluzi praćenja. Postavi praćenje pojedinačnih unosa manga putem gumba za praćenje. + Ovo proširenje nije iz službenog popisa. Neslužbeno Provjeri nove naslovnice i pojedinosti prilikom aktualiziranja biblioteke Automatski osvježi metapodatke @@ -465,9 +462,9 @@ Tema Datum dodavanja - %d praćenje - %d praćenja - %d praćenja + %d usluga praćenja + %d usluge praćenja + %d usluga praćenja Nemaš označenih izvora Gotovo @@ -477,7 +474,7 @@ Izbriši poglavlja Izvori ovog proširenja mogu sadržavati neprikladan sadržaj (18+) 18+ - Praćenja bez prijave: + Usluge praćenja bez prijave: To ne sprečava neslužbene ili potencijalno krivo označena proširenja prikazati neprikladan sadržaj (18+) unutar aplikacije. Preskače se %d poglavlje. Ne postoji u izvoru ili je filtrirano @@ -571,8 +568,8 @@ Isključeno Uključeno Postavke kategorija za sortiranje - Aktualiziraj pratioce prilikom aktualiziranja biblioteke - Automatski osvježi pratioce + Aktualiziraj usluge praćenja prilikom aktualiziranja biblioteke + Automatski aktualiziraj usluge praćenja Ograničenja: %s Pokreni preuzimanje sada Neki proizvođači imaju dodatna programska ograničenja koja onemogućuju pozadinske usluge. Ova web-stranica sadrži daljnje informacije o tome kako to popraviti. @@ -624,8 +621,8 @@ Ažuriranja aplikacije Nema se što raščistiti S nepročitanim poglavljima - Poboljšane usluge - Usluge koje pružaju poboljšane značajke za određene izvore. Unosi se automatski prate kada se dodaju u biblioteku. + Poboljšane usluge praćenja + Pruža poboljšane značajke za određene izvore. Unosi se automatski prate kada se dodaju u biblioteku. Prati Politika privatnosti Preskoči ažuriranje unosa @@ -655,7 +652,7 @@ Preskočeno Preskočena aktualiziranja: %1$d Preokrenuto uspravno - Pomakni serijal na vrh + Pomakni seriju na vrh Deaktivirano Nema unosa u biblioteci za spremanje u sigurnosnu kopiju Poboljšava performanse čitača @@ -677,7 +674,6 @@ Zadnja provjera aktualiziranja Rastavi visoke slike (BETA) Samo na mrežom bez ograničenja - Kad se baterija nije slaba Broj nepročitanih Izbriši kategoriju Želiš li izbrisati kategoriju „%s”\? @@ -724,7 +720,7 @@ Nema opisa Unosi Globalno aktualiziranje - Pratitelji + Usluge praćenja Srednja ocjena Korišteno %dd @@ -827,10 +823,17 @@ Sinkroniziranje biblioteke završeno Dodirni ovdje za pomoć s Cloudflareom Ispušteno\? Zadnjih 20 dana i 2 mjeseca - Zapis praćenja + Prijava za uslugu praćenja Prekoraöeno razdoblje provjere Provjera zadnjih 10 i više dana Indeks preuzimanja poništen Nije bilo moguće stvoriti datoteku sigurnosne kopije Licencirano – Nema poglavlja za prikaz + Otključaj %s + Ne postoji veza s internetom + HTTP %d, provjeri web stranicu u WebView + Nije bilo moguće povezati se s računalom %s + Pomakni seriju na kraj + Relativne vremenske oznake + „%1$s” umjesto „%2$s” \ No newline at end of file diff --git a/i18n/src/main/res/values-hu/strings.xml b/i18n/src/main/res/values-hu/strings.xml index 071bdd1d5..9df1cbc82 100644 --- a/i18n/src/main/res/values-hu/strings.xml +++ b/i18n/src/main/res/values-hu/strings.xml @@ -148,8 +148,6 @@ Összes engedélyezése Kiválasztás megfordítása Menü - A befejezéshez nyomd meg újfent a Vissza gombot - Tachiyomi feloldása A könyvtárad üres Nincs új frissítés Nincs letöltés folyamatban @@ -460,7 +458,6 @@ Beállítás alapértelmezettként Tablet mód Lista elejére - Nézze meg a web oldalt WebView-ban Globális keresés \"%1$s\"-ra/re Nincs több találat Nincs találat @@ -614,7 +611,6 @@ Szűri a könyvtár összes tartalmát Hiba a fedlap megosztása közben Bezár - Ha nem alacsony az akkumulátor tözöttség Fordított álló Megállítva Olvasási lista diff --git a/i18n/src/main/res/values-in/strings.xml b/i18n/src/main/res/values-in/strings.xml index e4c7a8520..cff2bb024 100644 --- a/i18n/src/main/res/values-in/strings.xml +++ b/i18n/src/main/res/values-in/strings.xml @@ -110,7 +110,7 @@ Kiri Kanan Tengah - Tipe rotasi bawaan + rotasi bawaan Bebas Terkunci tegak Terkunci mendatar @@ -129,7 +129,7 @@ Bab keempat dari terakhir dibaca Bab kelima dari terakhir dibaca Unduh bab baru - Layanan + Pelacakan Buat cadangan Dapat digunakan untuk memulihkan isi pustaka saat ini Pulihkan cadangan @@ -248,11 +248,11 @@ Tidak terpercaya Lepas Ekstensi tidak terpercaya - Ekstensi ini ditanda tangani dengan sertifikat yang tidak terpercaya dan belum diaktifkan. + Ekstensi ini ditandatangani dengan sertifikat tidak tepercaya dan tidak diaktifkan. \n -\nSebuah ekstensi berbahaya dapat membaca kredensial login Anda yang tersimpan pada Tachiyomi atau mengeksekusi kode berbahaya. +\nEkstensi berbahaya dapat membaca kredensial login apa pun yang disimpan atau mengeksekusi kode arbitrer. \n -\nDengan mempercayai sertifikat ini Anda menyetujui resiko tersebut. +\nDengan mempercayai sertifikat ini, Anda menerima risiko ini. Kecepatan animasi ketukan dua kali Tanpa animasi Normal @@ -306,7 +306,6 @@ Ikuti sistem Kelola notifikasi Keamanan dan privasi - Buka Tachiyomi Menu Terbaru Terlama @@ -355,7 +354,7 @@ Sumber Pembaruan ekstensi Membaca - WebView dibutuhkan untuk Tachiyomi + WebView diperlukan agar aplikasi dapat berfungsi Terdapat %d perbaruan ekstensi @@ -366,7 +365,6 @@ Selengkapnya Di pustaka Tambahkan ke pustaka - Cek dalam WebView Lisensi terbuka Gagal memulihkan data dari cadangan Pencadangan data gagal @@ -375,7 +373,6 @@ Hilangkan tanda Tandai Pilih kebalikan - Tekan lagi untuk keluar Hanya yang sudah diunduh Tersemat Situs web @@ -403,8 +400,8 @@ Selesai dalam %1$s dengan %2$s kesalahan - Sinkronisasi satu arah untuk memperbarui kemajuan bab dalam layanan pelacakan. Siapkan pelacakan untuk entri individu dari tombol pelacakan mereka. - Ekstensi ini bukan dari daftar ekstensi resmi Tachiyomi. + Sinkronisasi satu arah untuk memperbarui kemajuan bab di layanan pelacak eksternal. Siapkan pelacakan untuk setiap entri dari tombol pelacaknya. + Ekstensi ini bukan dari daftar resmi. Tidak resmi Periksa sampul dan detail baru saat memperbarui pustaka Segarkan metadata secara otomatis @@ -526,7 +523,7 @@ Layar mendatar Layar Tegak Entri dalam kategori yang dikecualikan tidak akan diunduh meskipun mereka juga termasuk dalam kategori yang disertakan. - Jenis rotasi + Rotasi Skala abu-abu Tampilkan zona ketukan Entri dalam kategori yang dikecualikan tidak akan diperbarui meskipun mereka juga termasuk dalam kategori yang disertakan. @@ -554,8 +551,8 @@ Beberapa pabrikan mempunyai batasan aplikasi tambahan yang mematikan layanan latar belakang. Website ini memiliki info lebih lanjut untuk memperbaikinya. Memperbarui pustaka... (%1$d/%2$d) Pencadangan/pemulihan mungkin tidak berfungsi jika Optimisasi MIUI dimatikan. - Layanan yang menyediakan fitur yang ditingkatan untuk sumber tertentu. Entri dilacak secara otomatis ketika ditambahkan ke pustaka Anda. - Layanan yang ditingkatkan + Menyediakan fitur yang disempurnakan untuk sumber tertentu. Entri secara otomatis dilacak ketika ditambahkan ke perpustakaan Anda. + Pelacak yang ditingkatkan Mode gelap hitam pekat yotsuba Yin dan Yang @@ -640,7 +637,6 @@ Membersihkan data WebView Data WebView telah dibersihkan Tutup - Saat baterai tidak lemah Sumber yang diinstal tidak ditemukan Tidak ada sumber yang ditemukan Pembaruan terakhir @@ -801,4 +797,11 @@ Pelacak login Tidak dapat membuat file cadangan Lisensi - Tidak ada chapter untuk ditampilkan + Buka kunci %s + Tidak ada koneksi Internet + HTTP %d, periksa situs web di WebView + Tidak dapat mencapai %s + Pindahkan seri ke bawah + Penanda waktu + \"%1$s\" seharusnya \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-it/strings.xml b/i18n/src/main/res/values-it/strings.xml index a987815e5..c4151930e 100644 --- a/i18n/src/main/res/values-it/strings.xml +++ b/i18n/src/main/res/values-it/strings.xml @@ -101,7 +101,7 @@ Sinistra Destra Centro - Orientamento predefinito + Rotazione predefinita Libero Bloccato verticale Bloccato orizzontale @@ -122,7 +122,7 @@ Quintultimo capitolo letto Scarica nuovi capitoli - Servizi + Servizi di tracking Cancella cache capitoli Usati: %1$s @@ -278,7 +278,7 @@ Estensione non attendibile Questa estensione è stata firmata con un certificato non attendibile e non è stata attivata. \n -\nUn\'estensione maliziosa potrebbe leggere credenziali di accesso all\'interno di Tachiyomi, o eseguire codice dannoso. +\nUn\'estensione maliziosa potrebbe leggere credenziali di accesso salvate o eseguire codice dannoso. \n \nFidandoti di questo certificato accetti questi rischi. Velocità animazioni doppio tocco @@ -362,7 +362,6 @@ Impossibile bypassare Cloudflare Per favore aggiorna l\'app WebView per una migliore compatibilità Aggiornamenti capitoli - Sblocca Tachiyomi Schermo sicuro nasconde i contenuti dell\'app quando cambi applicazione e blocca gli screenshot Visualizzazione Aggiornamenti estensione @@ -390,7 +389,6 @@ Per %d voci Cercando nuovi capitoli - Controlla il sito in WebView Indirizzo e-mail Ottimizzazione batteria già disattivata Facilita gli aggiornamenti e i backup in secondo piano @@ -425,9 +423,8 @@ Ripristino già in corso Backup fallito Il backup è già in corso - Premi indietro nuovamente per uscire Solo scaricati - È necessaria l\'app WebView per Tachiyomi + WebView è necessaria per il funzionamento dell\'app Modalità di lettura Per questa serie Guida alle fonti locali @@ -447,7 +444,7 @@ Completato in %1$s con %2$s errori %02d min, %02d sec - Sincronizzazione a senso unico per aggiornare l\'avanzamento del capitolo sui servizi di tracking. Imposta il tracking per le singole voci dai loro pulsanti di tracking. + Sincronizzazione a senso unico per aggiornare l\'avanzamento dei capitoli sui servizi di tracking. Imposta il tracking per le singole voci dai loro pulsanti di tracking. Riduce il banding, ma potrebbe influire sulle prestazioni %d categoria @@ -462,7 +459,7 @@ Fonti mancanti: Il backup non contiene alcuna voce di libreria. File di backup invalido - Questa estensione non è presente nella lista ufficiale delle estensioni di Tachiyomi. + Questa estensione non è dalla lista ufficiale. Non ufficiale Controlla nuove copertine e descrizioni durante l\'aggiornamento della libreria Ricarica metadati automaticamente @@ -577,7 +574,7 @@ Tocca per vedere i dettagli Questa versione di Android non è più supportata Impossibile copiare negli appunti - Orientamento + Rotazione Disabilita modalità incognito Orizzontale Verticale @@ -636,8 +633,8 @@ Predefinita Attività in secondo piano Le funzioni di backup e ripristino potrebbero non funzionare correttamente se le ottimizzazioni MIUI sono disabilitate. - Servizi che offrono funzioni migliorate per fonti specifiche. Le voci sono tracciate automaticamente quando aggiunte alla libreria. - Servizi migliorati + Offrono funzioni migliorate per fonti specifiche. Le voci sono tracciate automaticamente quando aggiunte alla libreria. + Servizi di tracking migliorati Aspetto Traccia Guida introduttiva @@ -697,7 +694,6 @@ Cancella dati WebView Dati WebView cancellati Chiudi - Quando la batteria non è scarica Nessuna fonte installata trovata Nessuna fonte trovata Conteggio non letti @@ -863,7 +859,14 @@ Sincronizzazione libreria Tocca qua per assistenza con Cloudflare Indice dei download invalidato - Login del tracking + Login del trakcer Non è stato possibile creare un file di backup Licenziato - Nessun capitolo da mostrare + HTTP %d, controlla il sito nella WebView + Nessuna connessione ad internet + %s non raggiungibile + Sblocca %s + Spostare la serie in fondo + Timestamp relativi + «%1$s» invece di «%2$s» \ No newline at end of file diff --git a/i18n/src/main/res/values-ja/strings.xml b/i18n/src/main/res/values-ja/strings.xml index 32156f9e3..9dfee3f0d 100644 --- a/i18n/src/main/res/values-ja/strings.xml +++ b/i18n/src/main/res/values-ja/strings.xml @@ -35,7 +35,7 @@ 既読にする 未読にする 削除する - ライブラリを更新する + ライブラリを更新 編集 カテゴリを追加 カテゴリを編集 @@ -118,7 +118,7 @@ 中央 通常 速い - 既定の回転モード + 既定の画面向き 自動回転 縦向き画面を強制 R @@ -165,7 +165,7 @@ 同期 この拡張機能は信頼できない証明書でサインされているため、有効にされていません。 \n -\n悪意のある拡張機能はTachiyomiに保存されているすべてのログイン情報を読み取ることや、任意コード実行をすることができます。 +\n悪意のある拡張機能は保存されているすべてのログイン情報を読み取ることや、任意コード実行をすることができます。 \n \nこれらのリスクを受け入れ、この証明書を信頼しますか? ページ数を表示 @@ -177,7 +177,7 @@ 最後に読んだ章の3番目 最後に読んだ章の4番目 最後に読んだ章の5番目 - サービス + トラッカー 復元が完了しました バックアップしたいのは? バックアップを復元中 @@ -317,7 +317,6 @@ %1$s分後 セキュア画面 - Tachiyomiのロックを解除する %1$d新しい章 @@ -325,7 +324,6 @@ 最新 ソース ローカルソース - WebViewでサイトを開く タブ メールアドレス @@ -354,7 +352,7 @@ ソースがありません: バックアップにはライブラリの項目が含まれません。 バックアップファイルは無効です - 一方同期の追跡サービスにある章の読書進捗を更新します。個別の項目の「同期」ボタンで追跡サービスを設定してください。 + 一方同期の外部追跡サービスにある章の読書進捗を更新します。個別の項目の「同期」ボタンで追跡サービスを設定してください。 余白 読書中 章の間の遷移ページを常に表示 @@ -366,7 +364,7 @@ 乗算 バンディングを軽減しますが、パフォーマンスを影響するかもしれません 画面の切り抜きエリアにも内容を表示 - この拡張機能はTachiyomiの公式拡張機能リストに含まれていません。 + この拡張機能は公式リストに含まれていません。 非公式 更新あり @@ -390,10 +388,9 @@ 全て有効にする 選択を反転 メニュー - もう一度押して終了します 章の更新 WebViewアプリを更新して互換性を向上させてください - TachiyomiはWebViewを必要としています + WebViewが必要です %d件の拡張機能の更新が利用可能 @@ -536,7 +533,7 @@ 章が見つかりませんでした シークレットモードを無効にする 追跡ガイド - 回転モード + 画面向き 自動 項目のタイトルに基づいてフォルダを作成 別々のフォルダにページを保存 @@ -592,8 +589,8 @@ Shizukuは実行中ではありません 項目数 拡張機能をインストール中… - 高度なサービス - 特定ソース専用の高度な機能を提供するサービスです。項目はライブラリに追加される時、自動で追跡され始めます。 + 高度な追跡サービス + 特定ソース専用の高度な機能を提供します。項目はライブラリに追加される時、自動で追跡され始めます。 ほかの場所にもバックアップのコピーを保管してください。 システムログにverboseログを出力(アプリのパフォーマンスが低下します) 大規模のアップデートはソースに有害で、ソースを遅くし、電池の消耗を増加する可能性があります。詳しくはタップでご覧ください。 @@ -640,7 +637,6 @@ 閉じる WebViewデータを消去 WebViewデータを消去しました - バッテリー残量が十分な場合 ソースが見つかりません インストール済みのソースが見つかりません 前回の更新確認 @@ -783,7 +779,7 @@ 今日、連載更新が予想されていないためスキップしました 間隔を設定 - 実績あり + 結果あり %s の追跡を削除しますか\? 毎に評価 ごとに更新するように設定する @@ -798,4 +794,14 @@ ライブラリを同期しました Cloudflareに関するヘルプ情報はこちら ダウンロード インデックスを消去しました + バックアップ ファイルを作成できませんでした + 追跡サービスにログイン + ライセンス制限あり—章を表示できません + HTTP %d、WebViewでこのWebサイトを確認してください + インターネット接続がありません + %sにアクセスできませんでした + %sをアンロック + シリーズを底に移動 + 相対的なタイムスタンプ + 「%2$s」の代わりに「%1$s」を使用 \ No newline at end of file diff --git a/i18n/src/main/res/values-jv/strings.xml b/i18n/src/main/res/values-jv/strings.xml index 2b4894dbc..14b9636aa 100644 --- a/i18n/src/main/res/values-jv/strings.xml +++ b/i18n/src/main/res/values-jv/strings.xml @@ -81,7 +81,6 @@ Durung kewoco Saringan Pengaturan - Mbukak Tachiyomi Sejarah Kategori Ora ono updetan anyar @@ -120,7 +119,6 @@ Kelacak Diwenehi tandha Menu - Pencet bali maneh kanggo metu Ngelacak Bab Sampeyan ora duwe kategori. Dhemok tombol tambah kanggo nggawe siji kategori kanggo ngatur perpustakaane sampeyan. @@ -260,7 +258,6 @@ Nganyari metadata otomatis Jaringan gak kebates tok Lewati updatean judul - Pas baterai ora titik Bohoso aplikasi Ekstensi iki ditandatangani nganggo sertifikat sing ora dipercaya lan ora diaktifake. \n diff --git a/i18n/src/main/res/values-ka-rGE/strings.xml b/i18n/src/main/res/values-ka-rGE/strings.xml index 0c733f0c9..11a537fe1 100644 --- a/i18n/src/main/res/values-ka-rGE/strings.xml +++ b/i18n/src/main/res/values-ka-rGE/strings.xml @@ -244,7 +244,6 @@ ჩანართები შედეგების სიის დასასრული შედეგი ვერ მოიძებნა - ვებსაიტის ნახვა WebView-ში ლოცალური წყარო სხვა ბოლოს გამოყენებული @@ -347,8 +346,6 @@ რეზერვი გავრცობები გავრცობის შესახებ - Tachiyomi-ს განბლოკვა - გასასვლელად დააჭირეთ ისევ უკან გლობალური ძებნა კატეგორიების მინიჭება ყდის რედაქტირება diff --git a/i18n/src/main/res/values-kk/strings.xml b/i18n/src/main/res/values-kk/strings.xml index 6fbad6634..58d841434 100644 --- a/i18n/src/main/res/values-kk/strings.xml +++ b/i18n/src/main/res/values-kk/strings.xml @@ -16,9 +16,7 @@ Көшу Әдепкі Ескерту - Tachiyomi-ді бұғаттан босату Өзгертуді растау үшін аутентификация өтіңіз - Шығу үшін қайтадан басыңыз Мәзір Сүзгі Бетбелгіленген @@ -50,7 +48,6 @@ Тізім Жүктелген тараулар Қолданба экранын қорғау - Батарея қуаты аз емес кезінде Қолданба кейпі Динамикалық Жасыл Алма @@ -419,7 +416,6 @@ Электрондық пошта мекенжайы Шығу Белгісіз қателік - Сайтты WebView-де тексеріңіз Дерекқорды тазалау Қателіктер туралы есептерді жіберу Сізде бекітілген дереккөздер жоқ diff --git a/i18n/src/main/res/values-km/strings.xml b/i18n/src/main/res/values-km/strings.xml index 5c3f7e44a..85cbee0ce 100644 --- a/i18n/src/main/res/values-km/strings.xml +++ b/i18n/src/main/res/values-km/strings.xml @@ -22,7 +22,6 @@ ប្រភពម៊េងហ្គា ជំនួយ លំនាំដើម - បើកតាឈិយ៉ូមិ ហ្វីលធឺ បានបញ្ចូលទៅក្នុងបណ្ណាល័យ បានដាក់ការតាមដានការអាន @@ -76,7 +75,6 @@ អ្នកពុំមានការទម្រៀបម៊េងហ្គាឡើយ។ ចុចប៊ូតុងសញ្ញាបូកដើម្បីទម្រៀបម៊េងហ្គាសម្រាប់បណ្ណាល័យរបស់អ្នក។ ម៊េងហ្គា Authenticateដើម្បីបញ្ចាក់ពីការកែប្រែ - ចុចថយម្ដងទៀតដើម្បីចេញ ការកំណត់ កន្លែងកំណត់ បញ្ចាក់ភាគមុនថាបានអាន diff --git a/i18n/src/main/res/values-kn/strings.xml b/i18n/src/main/res/values-kn/strings.xml index b6857e6d7..ed01f878f 100644 --- a/i18n/src/main/res/values-kn/strings.xml +++ b/i18n/src/main/res/values-kn/strings.xml @@ -93,8 +93,6 @@ ಸೋಸು ಸಲಹಾಕಾರ ಸಂಯೋಜನೆಗಳು - ನಿರ್ಗಮಿಸಲು ಮತ್ತೊಮ್ಮೆ ಒತ್ತಿರಿ - ತಚಿಯೋಮಿಯನ್ನು ಅನ್ಲಾಕ್ ಮಾಡಿ ಇತಿಹಾಸ ಟ್ರ್ಯಾಕಿಂಗ್ ಅಧ್ಯಾಯಗಳು @@ -363,7 +361,6 @@ ಕೊನೆಯದಾಗಿ ಉಪಯೋಗಿಸಿದ ಇತರೆ ಲೋಕಲ್ ಮೂಲ - ವೆಬ್‌ವೀಕ್ಷಣೆಯಲ್ಲಿ ವೆಬ್‌ಸೈಟ್ ಪರಿಶೀಲಿಸಿ ಯಾವುದೇ ಫಲಿತಾಂಶಗಳು ಕಂಡುಬರಲಿಲ್ಲ ಹೆಚ್ಚಿನ ಫಲಿತಾಂಶಗಳಿಲ್ಲ ವರ್ಗವನ್ನು ನವೀಕರಿಸಲಾಗುತ್ತಿದೆ diff --git a/i18n/src/main/res/values-ko/strings.xml b/i18n/src/main/res/values-ko/strings.xml index 9aa1f7d3f..e41a78f64 100644 --- a/i18n/src/main/res/values-ko/strings.xml +++ b/i18n/src/main/res/values-ko/strings.xml @@ -66,8 +66,8 @@ 정보 격자 크기 데이터 이전 - 확장기능 - 확장기능 정보 + 확장 앱 + 확장 앱 정보 전체 검색 일시중지 세로 @@ -92,10 +92,10 @@ 신뢰 신뢰되지않음 삭제 - 신뢰할 수 없는 확장기능 - 이 확장기능은 신뢰할 수 없는 인증서로 서명되어 활성화되지 않았습니다. + 신뢰할 수 없는 확장 앱 + 이 확장앱은 신뢰할 수 없는 인증서로 서명되어 활성화되지 않았습니다. \n -\n일부 악의적인 확장기능은 Tachiyomi에 저장된 로그인 정보를 읽거나 임의의 코드를 실행할 수도 있습니다. +\n일부 악의적인 확장 앱은 Tachiyomi에 저장된 로그인 정보를 읽거나 임의의 코드를 실행할 수도 있습니다. \n \n이 인증서를 신뢰하면 이러한 위험에 노출될 수 있습니다. 전체화면 @@ -179,7 +179,7 @@ %1$s화 다운로드 중 (%1$d/%2$d) 오류 - 일시중지됨 + 일시정지 회차 번호 소스 기준 회차 번호 기준 @@ -292,8 +292,6 @@ 도움말 이메일 주소 웹사이트 - Tachiyomi 잠금 해제 - 뒤로 가기를 한 번 더 누르면 앱을 종료합니다 메뉴 더 보기 소스 @@ -451,7 +449,7 @@ 디스플레이 필터링된 회차 건너뛰기 매우 높음 - 확장기능 업데이트 + 확장 앱 업데이트 기본값으로 설정 알림 설정 서재의 모든 항목에 적용 @@ -465,9 +463,9 @@ 회차 업데이트 포함: %s 백그라운드 활동 - 안 읽은 회차가 있을 때만 + 안 읽은 회차가 있음 카테고리 별 정렬 설정 - Shizuku를 확장기능 인스톨러로 사용하려면 Shizuku를 먼저 설치해 주세요. + Shizuku를 확장 앱 인스톨러로 사용하려면 Shizuku를 먼저 설치해 주세요. 전부 업데이트 백업에 항목이 포함되어 있지 않습니다. 개발자와 공유할 수 있는 오류 로그 파일을 생성합니다 @@ -482,7 +480,6 @@ 일부 제조사는 백그라운드 서비스를 종료하는 추가적인 제한 사항이 있습니다. 자세한 사항은 웹사이트를 참조하세요. 태블릿 UI - WebView에서 사이트 열기 핀 설정됨 \"%1$s\"를 전체 검색합니다 로컬 저장소 사용법 @@ -518,21 +515,21 @@ 읽지 않은 만화를 건너 뛰었습니다 이 안드로이드 버전은 더이상 지원되지 않습니다 - %d개의 확장기능 업데이트가 있습니다 + %d개의 확장 앱 업데이트가 있습니다 Cloudflare를 통과하지 못했습니다 - Tachiyomi를 사용하려면 WebView가 필요합니다 + Tachiyomi 앱의 기능을 사용하려면 WebView가 필요합니다 호환성을 위해 WebView 어플리케이션을 업데이트 해 주세요 넓은 이미지 이동 이 확장 프로그램은 더 이상 사용할 수 없습니다. 제대로 작동하지 않을 수 있으며 앱에 문제가 발생할 수 있습니다. 제거하는 것이 좋습니다. - 확장기능 설치 중… + 확장 앱 설치 중… 레거시 트래킹 서재 표지 새로고침 오류 로그 공유 회차를 찾을 수 없습니다 결과가 없습니다 - 이 확장기능의 소스는 19금 콘텐츠가 포함될 수 있습니다 + 이 확장 앱의 소스는 성인 컨텐츠가 포함될 수 있습니다 트래커 사용 시작 지금 다운로드 시작 @@ -556,7 +553,7 @@ 지원 종료 검색 결과가 없습니다 넓은 페이지 분할 시 배치가 읽는 방향과 다를 경우 - 19금 + 성인 컨텐츠 카테고리 탭 보이기 챕터를 가져온 날짜순 전체 항목 개수순 @@ -570,10 +567,10 @@ 오래된 순 옥색 딸기 칵테일 - 확장기능 목록 취득 실패 + 확장 앱 목록 가져오기 실패 서재 업데이트 시 트래커 갱신 제외: %s - 이 확장기능은 공식 확장기능이 아닙니다. + 이 확장앱은 공식 확장앱이 아닙니다. 트래커 서비스에 항목 진행 상황을 동기화합니다. 트래킹 버튼을 이용하여 각각의 항목 별로 트래킹을 설정하세요. 트래커 가이드 향상된 서비스 @@ -616,11 +613,11 @@ 새로고침 청사과 문어 - 음과 양 + Yin & Yang 요츠바 - 후방주의 (19금) 소스 + 성인 콘텐츠 소스 소스 및 확장 기능 목록에 보이기 - 이 옵션을 끄더라도 비공식 또는 분류가 잘못된 확장 기능으로 인하여 후방주의 (19금) 컨텐츠가 표시될 수 있습니다. + 이 옵션을 끄더라도 비공식 또는 분류가 잘못된 확장 기능으로 인하여 성인 컨텐츠가 표시될 수 있습니다. 터치하여 자세히 보기 3일 제한: %s @@ -642,7 +639,6 @@ 마지막으로 업데이트한 날짜순 읽지 않은 항목 개수순 닫기 - 배터리가 부족하지 않을 때만 WebView 데이터 지우기 WebView 데이터 삭제됨 리더 성능 향상 @@ -690,7 +686,7 @@ 읽을 때 자동 다운로드 마지막 서재 업데이트: %s - 현재 회차 + 다음 회차가 이미 다운로드된 경우에만 작동됩니다 + 현재 회차 + 다음 회차가 이미 다운로드된 경우에만 작동됩니다. 인기 정말로 실행합니까\? 만화에 업데이트가 필요하지 않음으로 건너뜀 @@ -792,4 +788,18 @@ 다운로드 인덱스를 제거함 다음 업데이트 예정 탭하여 Cloudflare에 관한 도움말 보기 + 일별 가져오기 (10일 이상) + %s를 잠금해제 + 항상 업데이트 하도록 설정 + 서재를 동기화합니다 + 백업 파일을 생성할 수 없습니다 + 서재가 동기화되었습니다 + 이 시리즈를 맨 아래로 이동 + 라이센스 제한 - 회차를 표시할 수 없습니다 + 인터넷에 연결되지 않음 + 포기했나요\? 20일 ~ 2달 이내 + 항상 평가하기 + 트랙킹 서비스 로그인 + HTTP %d, WebView의 웹 사이트를 확인해 주세요 + %s에 연결할 수 없습니다 \ No newline at end of file diff --git a/i18n/src/main/res/values-lt/strings.xml b/i18n/src/main/res/values-lt/strings.xml index 142dfc7f1..820fc3087 100644 --- a/i18n/src/main/res/values-lt/strings.xml +++ b/i18n/src/main/res/values-lt/strings.xml @@ -3,8 +3,6 @@ Filtras Meniu Nustatymai - Paspauskite atgal, kad išeitumėte - Atrakinkite Tachiyomi Istorija Sekimas Skyriai @@ -458,7 +456,6 @@ Ištrinti viską InternalError: Patikrinkite klaidų žurnalus, kad gautumėte daugiau informacijos Kai duomenys neapmokestinami - Kai baterija nėra išsikrovusi Perkelti seriją į viršų Sekimo priemonės, neprijungtos prie: Pagerina skaitytuvo našumą @@ -609,7 +606,6 @@ Viršelis Prisegtas El. pašto adresas - Patikrinkite svetainę \"WebView\" aplinkoje Pasaulinė paieška… Naujausia Vietos šaltinių vadovas diff --git a/i18n/src/main/res/values-lv/strings.xml b/i18n/src/main/res/values-lv/strings.xml index c71d8f0dd..899565ddf 100644 --- a/i18n/src/main/res/values-lv/strings.xml +++ b/i18n/src/main/res/values-lv/strings.xml @@ -71,7 +71,7 @@ Kļūda Lejupielādētājs Lai uzlabotu saderību, lūdzu, atjauniniet WebView lietotni - Tachiyomi ir nepieciešams WebView + Lai aplikācija strādātu ir nepieciešams WebView Dublējuma atjaunošana neizdevās Dublējuma atjaunošana Atjaunošana jau notiek @@ -90,7 +90,6 @@ Dublējumā nav neviena bibliotēkas ieraksta. Paplašinājumu informācija Paplašinājumi - Atslēgt Tachijomi Abi Vertikāls Horizontāls @@ -116,7 +115,6 @@ Pievienošanas datums Jaunākā nodaļa Izvēlne - Vēlreiz nospiediet atpakaļ, lai izietu Jums nav nevienas kategorijas. Pieskarieties plus pogai, lai izveidotu kategoriju savas bibliotēkas organizēšanai. Jūsu bibliotēka ir tukša Nekas nesen lasīts @@ -131,7 +129,7 @@ Lejuplādēt Ainava Portrets - Vienumi rindā + Režģa izmērs Displejs Slēpt paziņojumu saturu Drošs ekrāns paslēpj saturu mainot lietotnes, un bloķē ekrānuzņēmumus @@ -247,7 +245,7 @@ Neuzticams paplašinājums Šis paplašinājums tika parakstīts ar neuzticamu sertifikātu, un tas netika aktivizēts. \n -\nĻaunprātīgs paplašinājums var nolasīt visus pieteikšanās akreditācijas datus, kas saglabāti Tachiyomi, vai izpildīt patvaļīgu kodu. +\nĻaunprātīgs paplašinājums var nolasīt visus saglabātos pieteikšanās akreditācijas datus vai izpildīt patvaļīgu kodu. \n \nUzticoties šim sertifikātam, jūs piekrītat šiem riskiem. Mantots @@ -280,11 +278,10 @@ Yotsuba Pilnīgi melns tumšais režīms Aplikācijas valoda - Ja akumalators nav zems Ar nelasītu(ām) nodaļu(ām) Kas nav sāktas Vienmēr jautāt - Kategorijas iestatījumi kārtošanai un rādīšanai + Kategorijas iestatījumi kārtošanai Izslēgto kategoriju ieraksti netiks atjaunināti, pat ja tie ir iekļautajās kategorijās. Neviens Iekļaut: %s @@ -295,7 +292,7 @@ Neoficiāls Neuzticams Šis paplašinājums vairs nav pieejams. Tas var nedarboties pareizi un var radīt problēmas ar lietotni. Ieteicams to atinstalēt. - Šis paplašinājums nav no oficiālo Tachiyomi paplašinājumu saraksta. + Šis paplašinājums nav no oficiālā saraksta. Sadalīt platas lapas Apvērst dalītās lapas izvietojumu Rādīt saturu izgriezuma apgabalā @@ -308,7 +305,7 @@ Ik pēc 2 dienam Ik pēc 3 dienām Tikai uz Wi-Fi - Automātisks + Auto Turēt ekrānu ieslēgtu Centrs Rotācijas tips @@ -341,7 +338,7 @@ Pelēks Atspējots Balts - Nepārtraukta vertikāle + Garā strīpā ar pārtraukumu Sānu platums Izslēgtās kategorijas Lokālais avots @@ -353,8 +350,8 @@ Mala Pa labi un pa kreisi Pa labi - No kreisās puses uz labo - Webtoon + Lapaspuses (no kreisās puses uz labo) + Garā strīpā Skāriena zonas Izstiept Ietilpt platumā @@ -364,8 +361,8 @@ Portrets Aizslēgts portreta režīmā Lasīšanas režīms - No labās puses uz kreiso - Vertikāls + Lapaspuses (no labās puses uz kreiso) + Lapaspuses (vertikāli) Bibliotēkas ieraksti Ātrs Noklusējuma rotācijas tips @@ -380,7 +377,7 @@ Izlaist filtrētās nodaļas Navigācija Invertēt skaļuma regulēšanas taustiņus - Rādīt ar ilgu pieskārienu + Rādīt darbīūbas ar ilgu pieskārienu Izveido mapes atbilstoši ieraksta nosaukumam Fona krāsa Mēroga tips @@ -409,7 +406,7 @@ Drukāt verbose žurnālus sistēmas žurnālā (samazina programmas veiktspēju) Atjaunot progresu pēc lasīšanas Izsekošanas rokasgrāmata - Saglabā avārijas žurnālu + Dalīties ar avārijas žurnālu Saglabā kļūdu žurnālus failā priekš koplietošanas ar izstrādātājiem Notīrīt nodaļas kešatmiņu Atsvaidzināt bibliotēkas vākus @@ -417,7 +414,7 @@ Nav bibliotēkas ierakstu, ko dublēt Atjaunošana atcelta Dublēšana/atjaunošana var nedarboties pareizi, ja ir atspējota MIUI Optimization. - Notīriet nodaļu kešatmiņu, aizverot lietotni + Notīriet nodaļu kešatmiņu, atverot lietotni %1$d ierakstu, kas nav bibliotēkas, ir datu bāzē Tīkls Dati @@ -482,7 +479,6 @@ Lapaspušu skats Izrakstīties Piesprausts - Pārbaudīt tīmekļa vietni šeit: WebView Citi Tagad jūs esat izrakstījies Cilnes @@ -632,9 +628,9 @@ Lietotnes atjauninājumi Ir pieejama jauna versija! Atjaunināti noklusējuma nodaļu iestatījumi - Palielināt ainavas attēlu + Automātiski tuvināt ainavas attēlus Nākošā lapa - Pieskaroties izplest platus attēlus + Izplest platus attēlus Nevar atvērt pēdējo lasīto nodaļu Pārlasīšana Vai dzēst lejupielādētās nodaļas\? @@ -703,7 +699,7 @@ Paisuma vilnis Vairāku Lejupielādēt uz priekšu - Darbojas tikai ar ierakstiem bibliotēkā un tad, ja pašreizējā un nākamā nodaļa jau ir lejupielādēta + Darbojas tikai ja pašreizējā nodaļa + nākošā nodaļa ir jau lejupielādēta. Jūsu bibliotēkā ir ieraksts ar tādu pašu nosaukumu. \n \nVai joprojām vēlaties turpināt\? @@ -745,7 +741,7 @@ Vienvirziena progresa sinhronizācija, uzlabota sinhronizācija Avoti, paplašinājumi, globālā meklēšana Tēma, datuma un laika formāti - Kategorijas, globāli atjauninājumi + Kategorijas, globāli atjauninājumi, nodaļu vilkšana Rādīt nelasīto skaitu uz atjauninājumu ikonas Logrīks nav pieejams, ja ir iespējota lietotņu bloķēšana RARv5 formāts netiek atbalstīts @@ -764,7 +760,7 @@ %1$s kļūda: %2$s Atjauninājums jau darbojas - %s radās neparedzēta kļūda. Mēs iesakām izveidot šo ziņojumu ekrānuzņēmumu, atrast avārijas žurnālu un pēc tam kopīgot to mūsu atbalsta kanālā Discord lietotnē. + %s radās neparedzēta kļūda. Mēs iesakām dalīties ar avārijas žurnālu mūsu atbalsta kanālā Discord lietotnē. Pagrieziet platas lapas, lai tās ietilptu Apvērst orientācija pagrieztām platām lapām Sadalīt augstus attēlus @@ -796,4 +792,45 @@ Globālajā atjauninājumā Kopā *obligāti + Nevarēja izveidot dublējuma failu + Lejupielādes indeks ir nederīgs + Licencēts - Nav nevienu nodaļu ko parādīt + Izlaists, jo šodien nebija gaidīta jauna nodaļa + Vilkt uz labās puses darbību + Atkļūdošanas informācija + Pieskarieties šeit, lai iegūtu palīdzību ar Cloudflare + Ir rezultāti + Noņemt arī no %s + Vilkt uz kreisās puses darbību + Novērtēt katru + Iestatīt intervālu + Pielāgots datu iegūšanas intervāls + Iegūt katru mēnesi (28 dienas) + Nokārtots pārbaudes periods + Nākamais gaidāmais atjauninājums + Pieteikšanās izsekošana + Sinhronizē bibliotēku + Bibliotēkas sinhronizācija ir pabeigta + Intervāli + + %d dienas + %d diena + %d dienas + + Šis noņems locālo izsekošanu. + Izdzēst lejupielādētos + Vai noņemt %s izsekošanu\? + OK + Ārpus paredzamā izlaišanas perioda + Dubult-pieskarieties, lai tuvinātu + %d katrā rindā + Atbloķēt %s + Par vēlu +10 pārbaudes + Pametāt\? Par vēlu +20 un 2 mēnešus + Vilkt nodaļas + Atjaunināt katru + Pielāgot intervālu + HTTP %d, pārbaudiet vietni iekš WebView + Nav interneta savienojuma + Nevarēja sasniegt %s \ No newline at end of file diff --git a/i18n/src/main/res/values-mr/strings.xml b/i18n/src/main/res/values-mr/strings.xml index a5974e747..9ae0f9de6 100644 --- a/i18n/src/main/res/values-mr/strings.xml +++ b/i18n/src/main/res/values-mr/strings.xml @@ -27,8 +27,6 @@ सामान्य मेनू बुकमार्क आहे - अॅप बंद करण्यासाठी बॅक पुन्हा दाबा - तचीयोमी उघडा ट्रॅकिंग लायब्ररी इंत्रिज श्रेण्या diff --git a/i18n/src/main/res/values-ms/strings.xml b/i18n/src/main/res/values-ms/strings.xml index 708306722..45fda159a 100644 --- a/i18n/src/main/res/values-ms/strings.xml +++ b/i18n/src/main/res/values-ms/strings.xml @@ -252,7 +252,7 @@ Sambungan tak dipercayai Sambungan ini telah ditandatangani dengan sijil tidak dipercayai dan ia tidak diaktifkan. \n -\nSambungan yang berniat jahat kemungkinan membaca sebarang kelayakan log masuk yang di simpan dalam Tachiyomi atau melaksanakan kod sesuka hati. +\nSambungan yang berniat jahat kemungkinan membaca sebarang kelayakan log masuk yang di simpan atau melaksanakan kod sesuka hati. \n \nDengan mempercayai sijil ini, anda menerima risiko tersebut. Kelajuan animasi ketik dua kali @@ -327,7 +327,6 @@ Gagal untuk memintas Cloudflare Sila kemas kini aplikasi WebView untuk keserasian yang lebih baik Pengemaskinian bab - Buka kunci Tachiyomi Skrin keselamatan menyembunyikan kandungan aplikasi apabila menukar aplikasi dan sekat tangkapan skrin Paparan @@ -358,7 +357,6 @@ %d kemas kini sambungan tersedia Kemas kini sambungan - Semak laman web dalam WebView Mengemas kini pustaka Membaca Sebaris panjang dengan sela @@ -366,7 +364,7 @@ Pilih songsang Sumber Tambahan sisi - WebView diperlukan untuk Tachiyomi + WebView diperlukan untuk aplikasi berfungsi Bab %1$s - %2$s Tambah penjejakan Tutup @@ -383,7 +381,6 @@ Sandaran sedang dijalankan Buang sematkan Sematkan - Tekan kembali sekali lagi untuk keluar Dimuat turun sahaja Terakhir digunakan Semak untuk kemas kini @@ -404,14 +401,14 @@ Selesai dalam %1$s dengan %2$s ralat - Sinkron satu hala untuk kemas kini bab kemajuan dalam sistem penjejakan. Sediakan penjejakan untuk entri individu daripada butang penjejakan mereka. + Menyelaras satu hala untuk kemas kini bab kemajuan dalam sistem penjejakan. Sediakan penjejakan untuk entri individu daripada butang penjejakan mereka. Segar semula muka hadapan pustaka Mengikut tarikh muat naik Data Tiada sumber: Sandaran tidak mengandungi apa-apa entri pustaka. Fail sandaran tidak sah - Sambungan ini bukan daripada senarai sambungan Tachiyomi yang rasmi. + Sambungan ini bukan daripada senarai yang rasmi. Tidak rasmi Semak muka hadapan dan tentang baharu bila mengemaskini pustaka Segar semula metadata secara automatik @@ -640,7 +637,6 @@ Hapus data WebView Data WebView dihapuskan Tutup - Apabila bateri tidak lemah Tiada sumber dipasang ditemui Tiada sumber ditemui Bilangan belum dibaca @@ -699,7 +695,7 @@ Kategori, Kemas kini keseluruhan, leret bab Mod membaca, paparan, navigasi Muat turun automatik, muat turun maju - Sinkron kemajuan satu hala, pertingkat sinkron + Menyelaras kemajuan satu hala, pertingkat menyelaras Manual & sandaran automatik Kumpulan log kerosakan, pengoptimuman bateri Kekunci aplikasi, skrin keselamatan @@ -784,7 +780,7 @@ %d hari - Tersuai Jarak Masa + Tersuai jarak masa Diluar jangkaan masa keluaran Anggaran setiap Melepasi tempoh semak @@ -794,4 +790,15 @@ Juga buang daripada %s Padam dimuat turun Mempunyai hasil + Tidak boleh menghasilkan fail sandaran + Berlesen - Tiada bab untuk ditayangkan + Tidak dapat mencapai %s + Menyelaraskan pustaka + Pustaka selesai diselaraskan + Tiada sambungan Internet + Log masuk penjejakan + Indeks muat turun tidak sah + Ketik di sini untuk bantuan berkenaan Cloudflare + HTTP %d, semak laman web dalam WebView + Buka kunci %s \ No newline at end of file diff --git a/i18n/src/main/res/values-nb-rNO/strings.xml b/i18n/src/main/res/values-nb-rNO/strings.xml index ff8522816..f1491264b 100644 --- a/i18n/src/main/res/values-nb-rNO/strings.xml +++ b/i18n/src/main/res/values-nb-rNO/strings.xml @@ -132,7 +132,7 @@ Ingen animasjon Normal Rask - Standard rotasjonstype + Standard rotasjon Fri Låst stående Låst liggende @@ -259,9 +259,9 @@ Rutenettstørrelse Denne utvidelsen ble signert med et usikkert sertifikat, og ble dermed ikke aktivert \n -\nEn skadelig utvidelse kan lese innloggingsdetaljer lagret i Tachiyomi, eller kjøre ukjent kode. +\nEn skadelig utvidelse kan lese alle lagrede innloggingsdetaljer eller kjøre vilkårlig kode. \n -\nVed å godta dette sertifikatet aksepterer du overstående risikoer. +\nVed å godta dette sertifikatet aksepterer du disse risikoene. Animasjonshastighet ved dobbelklikk Sider Bruk dette bildet som omslag\? @@ -323,7 +323,6 @@ Sikker skjerm Kapitteloppdateringer - Lås opp Tachiyomi Kapittel %1$s E-postadresse I biblioteket @@ -338,7 +337,7 @@ Meny Kilder Utvidelsesoppdateringer - Tachiyomi krever WebView + Appen fungerer ikke uten WebView Legg til sporing Mindre Mer @@ -351,7 +350,6 @@ Hopp over filtrerte kapitler Skjul merknadsinnhold Sikker skjerm skjuler programinnhold ved bytting av programmer og blokker skjermavbildninger - Trykk \"Tilbake\" igjen for å avslutte Kun nedlastet Uoffisiell Lesemodus @@ -394,7 +392,6 @@ Ukjent status Ukjent forfatter Lokal kildeguide - Sjekk nettsted i WebView %1$s gjenstående %1$s gjenstående @@ -414,7 +411,7 @@ Gjort på %1$s med %2$s feil Vis lesemodus - Denne utvidelsen kommer ikke fra den offisielle Tachiyomi-utvidelseslisten. + Denne utvidelsen kommer ikke fra den offisielle listen. deaktivere deaktiver alle aktiver alle @@ -535,7 +532,7 @@ Denne Android-versjonen støttes ikke lenger Liggende Stående - Rotasjonstype + Rotasjon Oppretter mapper i henhold til oppføringenes tittel Lagre sider i egne mapper Handlinger @@ -652,7 +649,6 @@ Lukk Slett WebView data WebView data slettet - Batteri er ikke lavt Vel, dette er pinlig Enveis fremdriftssynkronisering, forbedret synkronisering Kilder, utvidelser, globalt søk @@ -779,7 +775,7 @@ Mangler %1$s kapittel Mangler %1$s kapitler - Skift retning på roterte brede sider + Vend orienteringen av roterte brede sider Roter brede sider slik at de passer Debug info %d per rad @@ -814,4 +810,12 @@ Biblioteksynkronisering fullført Trykk her for å få hjelp med Cloudflare Nedlastingsindeksen er ugyldiggjort + Kunne ikke opprette en backup-fil + Lisensiert - Ingen kapitler å vise + Sporingsinnlogging + Lås opp %s + Flytt serien til bunnen + Ingen internettforbindelse + HTTP %d, sjekk nettsiden i WebView + Kunne ikke nå %s \ No newline at end of file diff --git a/i18n/src/main/res/values-ne/strings.xml b/i18n/src/main/res/values-ne/strings.xml index 9c479844f..07c53c4d6 100644 --- a/i18n/src/main/res/values-ne/strings.xml +++ b/i18n/src/main/res/values-ne/strings.xml @@ -3,7 +3,7 @@ अन अफ अँध्यारो मोड - थिम + थीम बारेमा उन्नत सेटिङहरू ट्र्याकिङ @@ -94,8 +94,6 @@ हालै केहि पढेको छैन हालैका कुनै अपडेट छैन डाउनलोड लाम - बाहिर निस्किन फेरि फिर्ता थिच्नुहोस् - ताचियोमी अनलक गर्नुहोस् कुनै डाउनलोडहरू छैन मद्दत एक्सटेन्शनको जानकारी @@ -164,7 +162,7 @@ मिडनाइट डस्क हरियो स्याउ गतिशील - एपको थिम + एपको थीम सिस्टम पालना गर्नुहोस् रूप अहिले डाउनलोड गर्न सुरु गर्नुहोस् @@ -172,7 +170,7 @@ क्रमबद्ध यी श्रृङ्खलाका सबै रद्द गर्नुहोस् लोकल स्रोत - WebViewमा खोल्नुहोस् + WebView मा खोल्नुहोस् परिवर्तन पुष्टि गर्न प्रमाणित गर्नुहोस् पूर्वनिर्धारित ट्याब्लेट UI @@ -202,7 +200,7 @@ स्थापना गरियो विश्वास यो एक्सटेन्शन अब उपलब्ध छैन। यसले राम्ररी काम नगर्न सक्छ र एपमा समस्या ल्याउन सक्छ। यसलाई अनइन्स्टल गर्न सिफारिस गरिन्छ। - यो एक्सटेन्शन आधिकारिक ताचियोमीको एक्सटेन्शन सूचीबाट होइन। + यो एक्सटेन्शन आधिकारिक सूचीबाट होइन। यस एक्सटेन्शनको स्रोतहरूमा NSFW (१८+) सामग्री समावेश हुन सक्छ एक्सटेन्शन स्थापना गर्दै… Shizuku चलिरहेको छैन @@ -265,9 +263,9 @@ अविश्वसनीय एक्सटेन्शन समावेश नगर्नुहोस्: %s इंस्टलर - यो एक्सटेन्शन एक अविश्वसनीय प्रमाणपत्र संग हस्ताक्षर गरिएको थियो र सक्रिय गरिएको थिएन। + यो एक्सटेन्शन अविश्वसनीय प्रमाणपत्र सँग हस्ताक्षर गरिएको थियो र सक्रिय गरिएको थिएन। \n -\nएउटा खराब एक्सटेन्शनले ताचियोमीमा भण्डारण गरिएका कुनै पनि लगइन प्रमाणहरू पढ्न वा स्वेच्छाचारी कोड कार्यान्वयन गर्न सक्छ। +\nएक खराब एक्सटेन्शनले कुनै पनि भण्डारण लगइन प्रमाणहरू पढ्न वा मनमानी कोड कार्यान्वयन गर्न सक्छ। \n \nयस प्रमाणपत्रमा विश्वास गरेर तपाईंले यी जोखिमहरू स्वीकार गर्नुहुन्छ। पूर्ण स्क्रिन @@ -302,8 +300,8 @@ सामान्य कुनै एनिमेसन छैन छिटो - पूर्वनिर्धारित रोटेशन प्रकार - रोटेशन प्रकार + पूर्वनिर्धारित रोटेशन + रोटेशन फ्री ल्याण्डस्केप लक गरिएको ल्याण्डस्केप @@ -473,7 +471,6 @@ कुनै परिणाम फेला परेन पिन गरिएको ग्लोबल रूपमा \"%1$s\" खोज्नुहोस् - WebView मा वेबसाइट जाँच गर्नुहोस् अज्ञात लेखक %1$s बाँकी @@ -629,7 +626,7 @@ त्रुटिहरू कुनै अध्याय नपढेकाले छोडियो ठूला अपडेटहरूले स्रोतहरूलाई हानि पुर्‍याउँछ र यसले ढिलो अपडेटहरू निम्त्याउन सक्छ र ब्याट्रीको प्रयोग पनि बढाउँछ। थप जान्न ट्याप गर्नुहोस्। - ताचियोमी को लागि WebView आवश्यक छ + एप काम गर्नका लागि WebView आवश्यक छ श्रृङ्खलालाई शीर्षमा सर्नुहोस् पोर्ट्रेट उल्ट्याउनु नपढिएका अध्यायहरू भएका कारण छोडियो @@ -661,7 +658,6 @@ पढ्दा स्वत: डाउनलोड गर्नुहोस् म्यानुअल र स्वचालित ब्याकअप एप लक, सुरक्षित स्क्रिन - जब ब्याट्री कम छैन अग्लो छविहरू विभाजित गर्नुहोस् (BETA) थुप्रै अपडेटहरू आइकनमा नपढिएको गणना देखाउनुहोस् @@ -673,7 +669,7 @@ डाउनलोड गरिएको मापन नगरिएको नेटवर्कमा मात्र अगाडि डाउनलोड गर्नुहोस् - थिम, मिति र समय ढाँचा + थीम, मिति र समय ढाँचा हालको अध्याय + अर्को पहिले नै डाउनलोड गरिएमा मात्र काम गर्दछ। समुन्द्री लहर संस्करण @@ -817,4 +813,9 @@ ट्र्याकिङ लगइन ब्याकअप फाइल सिर्जना गर्न असफल भयो लाइसेन्स प्राप्त - देखाउन को लागि कुनै अध्याय छैन + इन्टरनेट जडान छैन + %s मा पुग्न सकिएन + HTTP %d, WebView मा वेबसाइट जाँच गर्नुहोस् + अनलक %s + श्रृङ्खलालाई तल सार्नुहोस् \ No newline at end of file diff --git a/i18n/src/main/res/values-nl/strings.xml b/i18n/src/main/res/values-nl/strings.xml index fbde722b2..b69b554d1 100644 --- a/i18n/src/main/res/values-nl/strings.xml +++ b/i18n/src/main/res/values-nl/strings.xml @@ -327,7 +327,6 @@ Kon Cloudflare niet omzeilen Gelieve de WebView-app bij te werken voor betere compatibiliteit Hoofdstukupdates - Ontgrendel Tachiyomi Veilig scherm modus verbergt de inhoud van de app bij het wisselen en blokkeert schermafbeeldingen Weergave Verplaats naar beneden @@ -337,7 +336,6 @@ Vastpinnen Bronnen Selectie omkeren - Druk nogmaals op terug om te verlaten Herstellen geannuleerd Herstellen mislukt Er wordt al een back-up hersteld @@ -373,7 +371,6 @@ Hulp bij lokale bronnen Vastgepind Laatst gebruikt - Bekijk website in WebView E-mailadres %1$s resterend @@ -668,7 +665,6 @@ Classificatie per leeftijd Grote afbeeldingen splitsen (BETA) Versie - Wanneer de batterij niet leeg is Taal van de applicatie Geen bibliotheek inzendingen om te backuppen Ongelezen aantal @@ -747,4 +743,78 @@ Dit gaat je eerder geselcteerde startdatum %s verwijderen Download verwijderen Overgeslagen omdat serie geen updates vereist. + %s is een onverwachte fout tegengekomen. We raden u aan de crashlogboeken te delen in ons ondersteuningskanaal op Discord. + Foutopsporingsinformatie + Voltooide titels + Buiten verwachte vrijgave periode + Interval instellen + Aangepast ophaalinterval + Maandelijks ophalen (28 dagen) + Controleperiode overschreden + Volgende verwachte update + Splits hoge afbeeldingen + Gelicenseerd - Geen hoofdstukken te laten zien + Draairichting van geroteerde brede pagina\'s omdraaien + Dubbeltik om te zoomen + Log-in volgen + Bibliotheek synchroniseren + Heeft resultaten + Dit zal jouw eerder geselecteerde einddatum weg halen van %s + Dit zal de tracking lokaal verwijderen. + Haal ook weg van %s + Overlay + Overzicht + Leesduur + Schat elke + Herstart de applicatie + Interval aanpassen + Download Index geïnvalideerd + + + + + Maak de downloadindex ongeldig + + + + + OK + Swipe naar de linker actie + Intervallen + Haal %s tracking weg\? + Ontgrendel %s + Swipe naar de juiste actie + %d per rij + Hoofdstuk veeg + Roteer brede pagina\'s zodat ze passen + Kon een backup-bestand niet creëren + Bibliotheeksynchronisatie beëindigd + Trackers + Overgeslagen omdat geen uitgave is verwacht vandaag + *vereist + Totaal + Late 10+ controle + Gevallen\? Late 20+ en 2 maanden + Lees + Gevolgde titels + Gemiddelde score + %du + U staat op het punt om \"%s\" te verwijderen uit uw bibliotheek + %ds + Kon bestandspad van pagina %d niet vinden + Titels + Open op GitHub + Geen titels gevonden in deze categorie + Geen internet verbinding + Kon niet %s bereiken + Gebruikt + N.v.t + Stel in om te updaten iedere + Tik hier voor hulp met Cloudflare + %dd + Category is leeg + %dm + Er is een nieuwe versie beschikbaar van de officiële uitgavens. Tik om te leren hoe u kunt migreren vanuit niet-officiële F-Droid-releases. + HTTP %d, check website in WebView + In globale update \ No newline at end of file diff --git a/i18n/src/main/res/values-nn/strings.xml b/i18n/src/main/res/values-nn/strings.xml index 56824eeff..ece30340f 100644 --- a/i18n/src/main/res/values-nn/strings.xml +++ b/i18n/src/main/res/values-nn/strings.xml @@ -12,7 +12,6 @@ Historikk Standard Åtvaring - Lås opp Tachiyomi Autentiser for å stadfesta endring Filter Bokmerket @@ -87,7 +86,6 @@ Ingen nylege oppdateringar Bibliotek Kjelder - Trykk tilbake igjen for å avslutta Innstillingar Meny Sett kategoriar @@ -267,7 +265,6 @@ Manglande kjelder: Sporingstenester ikkje logga inn i: Nokre produsentar har ytterlegare appavgrensingar som drep bakgrunnstenester. Denne nettstaden har meir info om korleis det kan fiksast. - Sjekk nettstad i WebView Nettstaden Sorter etter Dato diff --git a/i18n/src/main/res/values-pl/strings.xml b/i18n/src/main/res/values-pl/strings.xml index c0b03e021..dd1ec001c 100644 --- a/i18n/src/main/res/values-pl/strings.xml +++ b/i18n/src/main/res/values-pl/strings.xml @@ -8,7 +8,7 @@ Kolejka pobierania Historia Ustawienia - Filtrowanie + Filtry Nieprzeczytane Szukaj Zaznacz wszystko @@ -16,7 +16,7 @@ Aktualizacje Kopia zapasowa i przywracanie Zakładki - Usuń filtrowanie + Usuń filtr Ostatnio czytane Oznacz jako przeczytane Oznacz jako nieprzeczytane @@ -138,7 +138,7 @@ Od lewej do prawej Od prawej do lewej Pionowy - Web-komiks + Długi pasek Domyślny styl czytania Dopasuj do ekranu Rozciągnij @@ -261,7 +261,7 @@ Niezaufane rozszerzenie To rozszerzenie było podpisane niezaufanym certyfikatem i nie zostało aktywowane. \n -\nZłośliwe rozszerzenie może odczytać dane logowania przechowywane w Tachiyomi albo uruchomić złośliwy kod. +\nZłośliwe rozszerzenie może odczytać dane logowania albo uruchomić złośliwy kod. \n \nUfając temu rozszerzeniu akceptujesz to zagrożenie. Zaznacz dane do zawarcia @@ -282,7 +282,7 @@ Otwórz używając WebView Kolor 32-bitowy Pomiń rozdziały ozn. jako przeczytane - Pokaż po przytrzymaniu + Pokaż akcje po przytrzymaniu Tryb mieszania kolorów Nakładka Pomnożenie @@ -305,7 +305,7 @@ Wylogowano Wstrzymane Zawsze pokazuj przejścia rozdziałów - Pionowo bez przerw + Pionowo z przerwami Pomijaj odfiltrowane rozdziały Wyświetlanie Ukryj zawartość powiadomienia @@ -338,14 +338,12 @@ Odwróć zaznaczenie Ostatni rozdział Menu - Odblokuj Tachiyomi Źródła Rozdział %1$s Aktualizowanie biblioteki Dla tej serii Mniej Dodaj do biblioteki - Sprawdź stronę w WebView Adres e-mail Filtruje wszystkie pozycje w twojej bibliotece Tylko pobrane @@ -372,7 +370,6 @@ Ukryj ekran Odepnij Przypnij - Wciśnij wstecz ponownie, aby wyjść Więcej Rozdz. %1$s - %2$s W bibliotece @@ -380,7 +377,7 @@ Ostatnio używane Przesyłanie postępu czytania do zewnętrznego serwisu. Ustaw śledzenie guzikiem \"Śledzenie\" w wybranych tytułach. Aktualizacje rozszerzeń - WebView jest wymagany do poprawnego działania Tachiyomi + WebView jest wymagany do poprawnego działania aplikacji Dostępna aktualizacja rozszerzenia Dostępne %d aktualizacje rozszerzeń @@ -430,7 +427,7 @@ Wykonano w %1$s z %2$s błędami Czytane - Te rozszerzenie nie jest na liście oficjalnych rozszerzeń Tachiyomi. + Te rozszerzenie nie jest na liście oficjalnych rozszerzeń. Nieoficjalne Po dacie dodania Dane @@ -562,7 +559,7 @@ Źródło nie jest wspierane Nieprzeczytane Orientacja ekranu - Automatyczne + Automatycznie Utwórz foldery względem tytułu Zapisz strony do osobnych folderów Akcje @@ -655,7 +652,7 @@ Posiada nieprzeczytane rozdziały FAQ i poradniki Brak przeczytanych rozdziałów - Przybliżaj poziome obrazy + Automatycznie przybliżaj poziome obrazy Pokaż Przesuń szeroki obraz Siatka z samych okładek @@ -691,7 +688,6 @@ Nie znaleziono źródła Nie znaleziono zainstalowanego źródła Cóż, to troszkę niezręczne - Kiedy poziom baterii nie jest niski Język Lista czytanych Nowa wersja jest dostępna z oficjalnych wydań. Naciśnij, by dowiedzieć się jak przeprowadzić migrację z nieoficjalnych wydań F-Droid. @@ -827,4 +823,20 @@ %d dni Synchronizowanie biblioteki + Nie można wykonać kopi zapasowej + Interwały + Usuń też z %s + HTTP %d, otwórz w WebView + Brak połączenia z internetem + Nie można połączyć się z %s + Przestań śledzić %s\? + Usuń pobrane + Synchronizacja biblioteki zakończona + Indeksy pobrań unieważnione + Naciśnij tutaj aby uzyskać pomoc dotyczącą Cloudflare + Odblokuj %s + Dostosowany interwał aktualizacji + W miesiącu (28 dni) + OK + To usunie śledzenie lokalne. \ No newline at end of file diff --git a/i18n/src/main/res/values-pt-rBR/strings.xml b/i18n/src/main/res/values-pt-rBR/strings.xml index 51217f683..0d0b9538b 100644 --- a/i18n/src/main/res/values-pt-rBR/strings.xml +++ b/i18n/src/main/res/values-pt-rBR/strings.xml @@ -112,7 +112,7 @@ Esquerda Direita Centro - Tipo de orientação padrão + Orientação padrão Automática Retrato bloqueado Paisagem bloqueado @@ -131,7 +131,7 @@ Quarto capítulo lido antes do último Quinto capítulo lido antes do último Fazer download de novos capítulos - Serviços + Monitoradores Criar backup Pode ser usado para restaurar a biblioteca atual Restaurar backup @@ -252,7 +252,7 @@ Extensão não confiável Esta extensão foi assinada com um certificado não confiável e não foi ativada. \n -\nUma extensão maliciosa poderia ler quaisquer credenciais de login armazenadas no Tachiyomi ou executar códigos arbitrários. +\nUma extensão maliciosa poderia ler quaisquer credenciais de login armazenadas ou executar códigos arbitrários. \n \nAo confiar neste certificado, você estará aceitando estes riscos. Velocidade da animação do toque duplo @@ -329,7 +329,6 @@ Falha ao contornar o Cloudflare Por favor, atualize o aplicativo de WebView para uma melhor compatibilidade Atualizações de capítulos - Desbloquear o Tachiyomi A tela segura oculta os conteúdos do aplicativo durante a troca de aplicativos e impede capturas de tela Visualização @@ -360,7 +359,7 @@ Menu Mais novos Mais antigos - Mover para o topo + Mover para o começo Mover para o final Atualização de extensão disponível @@ -368,7 +367,6 @@ %d atualizações de extensão disponíveis Atualizações de extensões - Verifique o site na WebView Atualizando a biblioteca Leitura Pular os capítulos filtrados @@ -384,8 +382,7 @@ Na biblioteca Menos Mais - Pressione voltar novamente para sair - A WebView é necessária para o Tachiyomi + A WebView é necessária para o funcionamento do app Licenças de código aberto Site Somente disponíveis offline @@ -420,9 +417,9 @@ Concluído em %1$s com %2$s erros Concluído em %1$s com %2$s erros - Sincronização unidirecional para atualizar o progresso dos capítulos nos serviços de monitoramento. Configure o monitoramento para itens individuais a partir de seus botões de monitoramento. + Sincronização unidirecional para atualizar o progresso dos capítulos nos serviços monitoradores externos. Configure o monitoramento para itens individuais a partir de seus botões de monitoramento. Atualizar as capas da biblioteca - Esta extensão não é da lista oficial de extensões do Tachiyomi. + Esta extensão não é da lista oficial. Não oficial Pela data de envio Dados @@ -549,7 +546,7 @@ Cria as pastas usando os títulos dos itens Salvar as páginas em pastas separadas Ações - Tipo de orientação + Orientação Nível de cinza Desabilitar o modo anônimo Auto @@ -586,8 +583,8 @@ Aurora-da-noite Maçã-verde Tema do aplicativo - Serviços que oferecem recursos aprimorados para fontes específicas. Os itens são automaticamente monitorados quando adicionados em sua biblioteca. - Serviços aprimorados + Oferecem recursos aprimorados para fontes específicas. Os itens são automaticamente monitorados quando adicionados em sua biblioteca. + Monitoradores aprimorados Dinâmico Atividade em segundo plano Mais baixa @@ -655,7 +652,7 @@ %1$d atualização(ões) falhou(aram) Toque para saber mais Retrato invertido - Mover série para o topo + Mover série para o começo Desativado Uma nova versão está disponível nos releases oficiais. Toque para saber como migrar dos releases não oficiais do F-Droid. Erro ao salvar a imagem @@ -664,7 +661,6 @@ Limpar os dados da WebView Dados da WebView limpos Fechar - Quando a bateria não está fraca Nenhuma fonte instalada foi encontrada Nenhuma fonte encontrada Contagem de não lidos @@ -830,7 +826,14 @@ Sincronização da biblioteca finalizada Toque aqui para obter ajuda com o Cloudflare Índice de downloads invalidado - Login do monitoramento + Login do monitorador Não foi possível criar o arquivo do backup Licenciado - Nenhum capítulo para mostrar + HTTP %d, verifique o site na WebView + Sem conexão de internet + Não foi possível resolver %s + Desbloquear o %s + Mover série para o final + Datas relativas + \"%1$s\" ao invés de \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-pt/strings.xml b/i18n/src/main/res/values-pt/strings.xml index 8ef841c58..f0a0dd8e5 100644 --- a/i18n/src/main/res/values-pt/strings.xml +++ b/i18n/src/main/res/values-pt/strings.xml @@ -359,7 +359,6 @@ Falha ao contornar o Cloudflare Por favor, atualize a app de WebView para melhor compatibilidade Atualizações de capítulos - Desbloquear o Tachiyomi Ecrã seguro esconde os conteúdos da app durante a troca de apps e impedir capturas de ecrã Visualização Capítulo %1$s e %2$d mais @@ -400,7 +399,6 @@ Atualizando biblioteca Fixado - Verifique o sítio web no WebView Preenchimento lateral Lendo Página longa com espaços @@ -415,7 +413,6 @@ Restauro já em progresso Cópia de segurança falhou Cópia de segurança já em progresso - Pressione novamente para sair Apenas transferidos WebView é necessária para Tachiyomi Cap. %1$s - %2$s @@ -676,7 +673,6 @@ Desativado Grelha apenas de capas Fechar - Quando a bateria não está fraca Mover séries para o topo Ignorado porque a séria está completa Ignorado porque nenhum capítulo foi lido @@ -847,4 +843,5 @@ Próxima atualização esperada Definido para atualizar a cada Pulado, pois nenhum lançamento é esperado para hoje + Deletar dowloand \ No newline at end of file diff --git a/i18n/src/main/res/values-ro/strings.xml b/i18n/src/main/res/values-ro/strings.xml index d004233c4..1fa29539a 100644 --- a/i18n/src/main/res/values-ro/strings.xml +++ b/i18n/src/main/res/values-ro/strings.xml @@ -314,7 +314,6 @@ Gestionați notificările Securitate Necesită deblocare - Deblochează Tachiyomi Ultimul capitol Blocați când este inactiv Întotdeauna @@ -368,7 +367,6 @@ %d actualizări de extensii sunt disponibile %d actualizări de extensii sunt disponibile - Verificați site-ul în WebView Se actualizează biblioteca Citind Săriți peste capitolele filtrate @@ -390,7 +388,6 @@ Pagină web Restaurarea copiei de rezervă a eșuat Crearea copiei de rezervă a eșuat - Apăsați din nou pentru a ieși Doar descărcate Restaurare anulată Restaurarea este deja în curs de desfășurare @@ -605,7 +602,6 @@ Ar trebui să păstrați copii ale backupurilor și în alte locuri. Îmbunătățește performanța cititorului Închide - Când bateria nu este scăzută Azi Listă de citit Lista de dorințe diff --git a/i18n/src/main/res/values-ru/strings.xml b/i18n/src/main/res/values-ru/strings.xml index 749bcea86..94ac19fe7 100644 --- a/i18n/src/main/res/values-ru/strings.xml +++ b/i18n/src/main/res/values-ru/strings.xml @@ -160,7 +160,7 @@ Растянуть Оценка Предпоследняя прочитанная глава - Сервисы + Сервисы отслеживания Установить как обложку Номер главы Название источника @@ -251,7 +251,7 @@ Ненадёжное расширение Это расширение было подписано ненадёжным сертификатом и не было активировано. \n -\nВредоносное расширение может считывать любые учетные данные для входа, хранящиеся в Tachiyomi, или выполнять произвольный код. +\nВредоносное расширение может считывать и хранить любые учётные данные для входа или выполнять произвольный код. \n \nДоверяя этому сертификату, вы принимаете эти риски. Скорость анимации при двойном нажатии @@ -354,7 +354,6 @@ Проверка наличия новых глав Обновление библиотеки - Проверить сайт в WebView Оптимизация батареи уже выключена Помогает с обновлением библиотеки и резервной копией в фоне Отключить оптимизацию батареи @@ -373,7 +372,6 @@ В начало В конец Обновления расширений - Разблокировать Tachiyomi Меню Источники Чтение @@ -385,9 +383,8 @@ Открепить Закрепить Добавить отслеживание - Нажмите ещё раз, чтобы выйти Только загруженные главы - Для Tachiyomi необходим WebView + Для работы приложения необходимо WebView Меньше Больше В библиотеке @@ -429,8 +426,8 @@ Выполнено за %1$s с %2$s ошибками Выполнено за %1$s с %2$s ошибками - Односторонняя синхронизация для обновления прогресса в сервисах отслеживания. Настройте отслеживание при помощи кнопки «Отслеживание». - Это расширение не входит в официальный список расширений Tachiyomi. + Односторонняя синхронизация для обновления прогресса в сторонних сервисах отслеживания. Настройте отслеживание при помощи кнопки «Отслеживание». + Это расширение не входит в официальный список расширений. Неофициальное Дата добавления Данные @@ -597,8 +594,8 @@ Обновление библиотеки… (%1$d/%2$d) У некоторых производителей есть дополнительные ограничения для приложений, которые убивают фоновые службы. На этом сайте есть более подробная информация о том, как это исправить. Резервная копия/Восстановление может не работать должным образом, если отключена «Оптимизация MIUI». - Сервисы, предоставляющие расширенные возможности для определённых источников. Серии будут автоматически отслеживаться при добавлении в библиотеку. - Расширенные сервисы + Предоставляет расширенные возможности для определённых источников. Серии будут автоматически отслеживаться при добавлении в библиотеку. + Расширенные сервисы отслеживания Динамическая Фоновая активность Самая низкая @@ -676,7 +673,6 @@ Данные WebView очищены Очистить данные WebView Закрыть - Когда батарея заряжена Не найдено установленных источников Не найдено источников Последняя проверка обновления @@ -825,7 +821,7 @@ Настроить интервал Задать интервал Проверка поздних 10+ дней - Прошедшая проверка периода + Срок проверки истёк Следующее ожидамое обновление За пределами ожидаемого периода выпуска Задать обновления каждые @@ -837,7 +833,7 @@ Пропущено, т.к сегодня не ожидается выпуска Удалить отслеживание %s\? - Это удалит отслеживание локально. + Это удалит отслеживание в приложении. Также удалить из %s ОК Удалить загруженное @@ -847,6 +843,13 @@ Нажмите здесь, чтобы получить помощь с Cloudflare Индекс загрузок недействителен Не удалось создать файл резервной копии - Лицензировано - Нет глав для показа + Лицензировано - Нет глав Вход в сервис отслеживания + HTTP %d, проверьте сайт в WebView + Нет подключения к интернету + Не удалось достичь %s + Разблокировать %s + Переместить серию в конец + Относительные временные метки + \"%1$s\" вместо \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-sa/strings.xml b/i18n/src/main/res/values-sa/strings.xml index f38bd56e4..25834e3e2 100644 --- a/i18n/src/main/res/values-sa/strings.xml +++ b/i18n/src/main/res/values-sa/strings.xml @@ -17,12 +17,10 @@ चरित्रम् मूलानि प्राथमिकम् - ताचियोमिम् उद्घाटयतु ग्रन्थालयः प्रतिलेखनं प्रतिसंस्कारं च पूर्वसूचना परिवर्तनं दृढीकर्तुं प्रमाणीकरोतु - निष्क्रमितुं पृष्ठगण्डं पुनः नोदयतु सूचिः शोधकम् पुटचिह्नं कृतानि @@ -555,7 +553,6 @@ \"%1$s\" कृते सार्वत्रिकेण अवेक्षताम् %1$s प्रति अन्तर्गच्छतु %1$s इतः बहिर्गच्छानि किम् - जालस्थानं वेबव्यू-तन्त्रांशे मार्गतु मूलं न प्रतिष्ठापितम् - %1$s माङ्गां ग्रन्थालये योजयानि किम् मुखचित्रनवीकरणम् अनुत्तीर्णम् diff --git a/i18n/src/main/res/values-sah/strings.xml b/i18n/src/main/res/values-sah/strings.xml index f1494aa88..486d9d40e 100644 --- a/i18n/src/main/res/values-sah/strings.xml +++ b/i18n/src/main/res/values-sah/strings.xml @@ -27,8 +27,6 @@ Биилтирэ Талба Түстэл - Өссө биирдэ төттөрүнү баттаан, тахсар курдук - Тachiyomi арыйыыта История Кэтээһин Түһүмэхтэр @@ -300,7 +298,6 @@ Бүтэһигинэн туттуллубут Атыттар Локальнай төрүт - Үөп сири WebView\'га көрүн Түмүк суох Эбии түмүк суох Кыбытыктар diff --git a/i18n/src/main/res/values-sc/strings.xml b/i18n/src/main/res/values-sc/strings.xml index 2391ca825..8f8040bcc 100644 --- a/i18n/src/main/res/values-sc/strings.xml +++ b/i18n/src/main/res/values-sc/strings.xml @@ -99,9 +99,9 @@ Estensione non afidàbile Custa estensione est istada firmada cun unu tzertificadu chi no est afidàbile e no est istada ativada. \n -\nUn\'estensione mala diat pòdere lèghere totu sas informatziones de atzessu sarvadas in Tachiyomi o fàghere eseguire còdighe arbitràriu. +\nUn\'estensione mala diat pòdere lèghere totu sas informatziones de atzessu sarvadas o fàghere esecutare còdighe arbitràriu. \n -\nPonende fidùtzia in custu tzertificadu atzetas custos arriscos. +\nPonende fide in custu tzertificadu atzetas custos arriscos. Ischermu intreu Ànima sas transitziones de sas pàginas Lestresa de s\'animatzione de su tocu dòpiu @@ -113,7 +113,7 @@ Navigatzione Teclas de su volume Fùrria sas teclas de su volume - Ammustra cun su tocu longu + Ammustra sas atziones cun su tocu longu Colore de isfundu Arbu/biancu Nieddu @@ -138,7 +138,7 @@ Peruna animatzione Normale Lestra - Casta predefinida de rotatzione + Rotatzione predefinida Lìbera Blocadu in verticale Blocadu in orizontale @@ -157,7 +157,7 @@ Cuartùrtimu capìtulu lèghidu Cuintùrtimu capìtulu lèghidu Iscàrriga sos capìtulos noos - Servìtzios + Arrastadores Crea una còpia de seguresa Podet èssere impreada pro ripristinare sa biblioteca atuale Riprìstina una còpia de seguresa @@ -328,7 +328,6 @@ Isfrancamentu de Cloudflare fallidu Agiorna s\'aplicatzione de WebView pro otènnere una cumpatibilidade prus manna Agiornamentos de sos capìtulos - Isbloca Tachiyomi S\'ischermu seguru cuat sos cuntenutos de s\'aplicatzione cando mudas de aplicatzione e blocat sas ischermadas Visualizatzione @@ -363,7 +362,6 @@ B\'ant agiornamentos disponìbiles pro %d estensiones Agiornamentos de sas estensiones - Verìfica su situ web cun WebView Agiornende sa biblioteca Leghende Brinca sos capìtulos filtrados @@ -379,10 +377,9 @@ Annanghe a sa biblioteca De mancu De prus - Pro Tachiyomi b\'at bisòngiu de WebView + Pro chi s\'aplicatzione funtzionet b\'at bisòngiu de WebView Litzèntzias a còdighe abertu Situ web - Incarca in segus torra pro essire Iscarrigados ebbia Cap. %1$s - %2$s Ùrtima impreada @@ -412,9 +409,9 @@ Fatu in %1$s cun %2$s errore Fatu in %1$s cun %2$s errores - Sincronizatzione a una diretzione ebbia pro agiornare su progressu in sos capìtulos in su servìtziu de arrastamentu. Imposta s\'arrastamentu pro sos elementos dae su butone de arrastamentu issoro. + Sincronizatzione a una diretzione ebbia pro agiornare su progressu in sos capìtulos in servìtzios de arrastamentu esternos. Imposta s\'arrastamentu pro sos elementos dae su butone de arrastamentu issoro. Annoa sas coberteddas de sa biblioteca - Custa estensione non benit dae sa lista de estensiones ufitziales de Tachiyomi. + Custa estensione non benit dae sa lista ufitziale. No ufitziale Pro data de carrigamentu Datos @@ -535,7 +532,7 @@ Custa versione de Android no est prus suportada Orizontale Verticale - Casta de rotatzione + Rotatzione Creat cartellas in base a su tìtulu de sos elementos Sarva sas pàginas in cartellas separadas Atziones @@ -567,8 +564,8 @@ Tako Agiornende sa biblioteca… (%1$d/%2$d) Sa còpia de seguresa e su riprìstinu diant pòdere non funtzionare comente si tocat si s\'otimizatzione MIUI est disabilitada. - Servìtzios chi frunint funtzionalidades avantzadas pro fontes ispetzìficas. Sos elementos benint arrastados in manera automàtica cando los annanghes a sa biblioteca tua. - Servìtzios avantzados + Frunit funtzionalidades avantzadas pro fontes ispetzìficas. Sos elementos benint arrastados in manera automàtica cando los annanghes a sa biblioteca tua. + Arrastadores avantzados Modalidade iscura niedda pura Yotsuba Yin e Yang @@ -652,7 +649,6 @@ Isbòida sos datos de WebView Datos de WebView isboidados Serra - Cando sa bateria no est bassa Ùrtimu agiornamentu Contu de non lèghidos Peruna fonte agatada @@ -809,4 +805,19 @@ %d dies Iscantzella sos iscarrigados + Toca inoghe pro agiudu cun Cloudflare + Isbloca %s + Sincronizende sa biblioteca + Non at s\'est pòdidu creare un\'archìviu de còpia de seguresa + Sincronizatzione de sa biblioteca acabada + Moe sa sèrie a fundu + Cun lissèntzia ufitziale - Perunu capìtulu de ammustrare + Peruna connessione a ìnternet + Ìnditze de sos iscarrigamentos invalidadu + Tenet resurtados + Atzessu a s\'arrastadore + Marcas temporales relativas + HTTP %d, verìfica su situ in WebView + \"%1$s\" in càmbiu de \"%2$s\" + Non s\'est pòdidu atzèdere a %s \ No newline at end of file diff --git a/i18n/src/main/res/values-sdh/strings.xml b/i18n/src/main/res/values-sdh/strings.xml index 5bc6ac7ee..65edfe421 100644 --- a/i18n/src/main/res/values-sdh/strings.xml +++ b/i18n/src/main/res/values-sdh/strings.xml @@ -7,7 +7,6 @@ سازکارییەکان لیستی داونلۆد تۆماری ڕابردوو - بکەرەوەTachiyomi دڵنیابوونەوە تا گۆڕانکاری بکرێت ئاماژەکردنی پێشوو وەک خوێندراو نیشانکردنی چاپتەر @@ -51,7 +50,6 @@ داونلۆد هیچ کاتیگۆرییەکت نییە. دەست بنێ بە دوگمەی زیادە تا کاتیگۆرییەک بۆ ڕێکخستنی تۆماری ڕابردووت درووست بکەیت. سڕینەوە - دووبارە گەڕانەوە بکە بۆ چوونە دەرەوە هێج نوێکارییەک نییە لەم دواییانەدا سەرچاوەکان هیچ مانگایەکت لەم دواییانە نەخوێندووەتەوە diff --git a/i18n/src/main/res/values-sk/strings.xml b/i18n/src/main/res/values-sk/strings.xml index 95a4fd998..a71d1433c 100644 --- a/i18n/src/main/res/values-sk/strings.xml +++ b/i18n/src/main/res/values-sk/strings.xml @@ -301,7 +301,6 @@ Najnovšia kapitola Zobraziť kapitoly Zrušiť všetky - Odomknúť Tachiyomi Menu Najnovšie Najstaršie @@ -323,7 +322,6 @@ Hľadať v nastaveniach Dátum pridaný Dátum načítania kapitoly - Stlačte tlačidlo späť znova pre ukončenie Prihlásiť sa pre potvrdenie zmien Predvolené Zdroje @@ -453,7 +451,6 @@ Čo je nové Pomôžte s prekladom Licencia Open Source - Skontrolujte webovú stránku vo WebView Chyba pri zdieľaní obalu %d sledovač @@ -483,7 +480,7 @@ Zoznam pozastavených položiek Stiahnuť dopredu Automatické sťahovanie počas čítania - Funguje iba na položkách v knižnici a ak je aktuálna kapitola a nasledujúca kapitola už stiahnutá + Funguje iba na položkách v knižnici a ak je aktuálna kapitola a nasledujúca kapitola už stiahnutá. Neznámy stav Prestávka Pridať sledovanie @@ -577,7 +574,7 @@ Inštaluje sa rozšírenie… Invertovať oblasti dotyku Uloženie stránok do samostatných priečinkov - Sivá + Šedivá Na šírku Obnovenie už prebieha Uložiť logy o chybách do súboru na zdieľanie s vývojármi @@ -612,7 +609,6 @@ Kapitoly %1$s a %2$d ďalšie Kapitoly %1$s a %2$d ďalších - Keď batéria nie je vybitá Typ rotácie Zatvoriť Záznamy vo vylúčených kategóriách nebudú aktualizované, aj keď sú tiež v zahrnutých kategóriách. @@ -662,7 +658,7 @@ Preskočenie %d kapitoly, buď zdroj chýba, alebo bol odfiltrovaný Preskočenie %d kapitol, buď zdroj chýba, alebo bol odfiltrovaný - Preskočenie %d kapitol, buď zdroj chýba, alebo bol odfiltrovaný + Zložky %d kapitoly, buď zdroj chýba, alebo boli filtrované Chyba pri ukladaní obrázka Aktualizácie aplikácie diff --git a/i18n/src/main/res/values-sq/strings.xml b/i18n/src/main/res/values-sq/strings.xml index 6476ad2ca..f992c611b 100644 --- a/i18n/src/main/res/values-sq/strings.xml +++ b/i18n/src/main/res/values-sq/strings.xml @@ -116,11 +116,9 @@ Sipas alfabetit Totali i kapitujve Paralajmërim - Zhblloko Tachiyomi Totali i hyrjeve Leximi i fundit Vërtetoni për të konfirmuar ndryshimin - Shtypni përsëri për të dalë Menuja Filtro Sipas numrit të kapitullit @@ -184,7 +182,6 @@ Ekrani i sigurt fsheh përmbajtjen e aplikacionit kur ndërron aplikacionet dhe bllokon screenshots Kjo nuk parandalon shtesat jozyrtare ose potencialisht të shënuara gabimisht që të shfaqin përmbajtjen NSFW (18+) brenda aplikacionit. Portret - Kur bateria nuk është e ulët Kufizimet: %s Përditësimet automatike Çdo 6 orë @@ -595,7 +592,6 @@ Kapitujt e shkarkuar Skedat Nuk ka më rezultate - Kontrolloni faqen e internetit në WebView Burimi lokal Tjetër Fiksuar diff --git a/i18n/src/main/res/values-sr/strings.xml b/i18n/src/main/res/values-sr/strings.xml index ec19957dd..0a66772ff 100644 --- a/i18n/src/main/res/values-sr/strings.xml +++ b/i18n/src/main/res/values-sr/strings.xml @@ -97,11 +97,11 @@ Неповерљиво Обриши Неповерљив додатак - Овај додатак је потписан са неповерљивим сертификатом и није активиран. + Овај додатак је потписан са непоузданим сертификатом и није активиран. \n -\nЗлонамерни додатак може прочитати све податке за пријаву који су спремљени у програму Тachiyomi или извршити произвољни код. +\nЗлонамерни додатак може прочитати све податке за пријаву или извршити произвољни код. \n -\nВерујући овом сертификату, прихваташ те ризике. +\nВерујући овом сертификату прихватате ове ризике. Цео екран Анимације при промени странице Брзина анимације двоклика @@ -257,7 +257,6 @@ Безбедносни екран Ажурирања на чекању - Откључај Tachiyomi Прикажи Сакриј обавештења Искључи оптимизацију коришћења батерије @@ -279,7 +278,6 @@ Закачи Изабери обрнуто Мени - Притисните поново назад да бисте изашли Само преузето Немате категорија. Додирните дугме плус да бисте направили нову категорију за организовање колекције. Поглавља %1$s @@ -344,7 +342,6 @@ Водич за локалне изворе Закачено Последње коришћено - Провери вебсајт у WebView Филтрира све наслове у колекцији Провери ажурирања Sajt @@ -365,7 +362,7 @@ Грешка Преузимања Ажурирајте WebView за бољу компатибилност - За Tachiyomi је потребан WebView + WebView је потребан како би апликација функционисала Неуспешно заобилажење Cloudflare-а Доступно је %d ново ажурирање додатака @@ -395,7 +392,7 @@ Прикажи начин читања Може садржати садржај за одрасле (18+) 18+ - Овај додатак није из званичне листе Таchiyomi додатака. + Овај додатак није из званичне листе. Незванично %d категорија @@ -662,7 +659,6 @@ Отвори на GitHub-у Грешка при чувању слике Затвори - Батерија није празна Обриши податке WebView-a Подаци WebView-a су обрисани Категорије, глобално ажурирање, листање поглавља @@ -833,4 +829,8 @@ Следеће очекивано ажурирање Синхронизовање колекције Месечно преузимање (28 дана) + Откључај %s + Нема везе са интернетом + HTTP %d, погледај веб сајт у WebView + %s је недоступан \ No newline at end of file diff --git a/i18n/src/main/res/values-sv/strings.xml b/i18n/src/main/res/values-sv/strings.xml index 4e37b056e..ef030d5de 100644 --- a/i18n/src/main/res/values-sv/strings.xml +++ b/i18n/src/main/res/values-sv/strings.xml @@ -99,7 +99,7 @@ Opålitlig extension Detta tillägg tecknades med ett otillförlitligt certifikat och var inte aktiverat. \n -\nEtt skadligt tillägg kan läsa inloggningsuppgifter som är lagrade i Tachiyomi eller utföra godtycklig kod. +\nEtt skadligt tillägg kan läsa lagrade inloggningsuppgifter eller utföra godtycklig kod. \n \nGenom att lita på detta certifikat accepterar du dessa risker. Fullskärm @@ -137,7 +137,7 @@ Ingen animering Normal Snabb - Standardrotationstyp + Standard rotation Upplåst Låst porträtt Låst liggande @@ -328,7 +328,6 @@ Det gick inte att kringgå Cloudflare Uppdatera WebView-appen för bättre kompatibilitet Kapiteluppdateringar - Lås upp Tachiyomi Säker skärm döljer appinnehållet när du byter app och blockerar skärmdumpar Skärmvisning @@ -364,7 +363,6 @@ %d tilläggsuppdateringar tillgängliga Uppdaterar biblioteket - Kontrollera webbplatsen i WebView Läser Hoppa över filtrerade kapitel Källor @@ -379,8 +377,7 @@ Välj omvänd Mindre Mer - Tryck tillbaka igen för att gå ur - WebView krävs för Tachiyomi + WebView krävs för att appen ska fungera Kapitel %1$s - %2$s Lokal källa guide Senast använd @@ -419,7 +416,7 @@ Saknade källor: Säkerhetskopian innehåller inga biblioteksposter. Ogiltig säkerhetskopia - Detta tillägg kommer inte från den officiella Tachiyomi-tilläggslistan. + Detta tillägg är inte från den officiella listan. Inofficiell Sök efter nytt omslag och detaljer när du uppdaterar biblioteket Uppdatera metadata automatiskt @@ -535,7 +532,7 @@ Kunde inte kopiera till urklipp Liggande Porträtt - Rotationstyp + Rotation Skapa mappar enligt posternas titel Spara sidor i separata mappar Åtgärder @@ -649,7 +646,6 @@ En ny version finns tillgänglig i de officiella utgåvorna. Tryck på för att lära dig hur du flyttar från inofficiella F-Droid-versioner. Inga biblioteksposter att säkerhetskopiera Öppna på GitHub - När batteriet inte är lågt Rensa data från WebView WebView-data rensas Stäng @@ -817,4 +813,9 @@ Kunde inte skapa en backup-fil Licensierad - Inga kapitel att visa Spårning av inloggning + Ingen internet anslutning + HTTP %d, kolla på webbsida i WebView + Kunde inte nå %s + Lås upp %s + Flytta serien till botten \ No newline at end of file diff --git a/i18n/src/main/res/values-te/strings.xml b/i18n/src/main/res/values-te/strings.xml index 633a47469..1ee8a80af 100644 --- a/i18n/src/main/res/values-te/strings.xml +++ b/i18n/src/main/res/values-te/strings.xml @@ -45,8 +45,6 @@ శోధకము వివరాలపట్టిక అమరికలు - నిష్క్రమించడానికి బ్యాక్ చిహ్నాన్ని మళ్ళీ నొక్కండి - టచియోమిని తెరవండి చరిత్ర కంట కనిపెట్టుట అధ్యాయాలు diff --git a/i18n/src/main/res/values-th/strings.xml b/i18n/src/main/res/values-th/strings.xml index 511635a60..a33e46839 100644 --- a/i18n/src/main/res/values-th/strings.xml +++ b/i18n/src/main/res/values-th/strings.xml @@ -104,7 +104,7 @@ ส่วนขยายที่ไม่น่าเชื่อถือ ส่วนขยายนี้ลงนามด้วยใบรับรองที่ไม่น่าเชื่อถือและจะไม่ถูกเปิดใช้งาน \n -\nส่วนขยายที่เป็นอันตรายสามารถอ่านข้อมูลการเข้าสู่ระบบที่จัดเก็บไว้ใน Tachiyomi หรือเรียกใช้โค้ดโดยอำเภอใจ +\nส่วนขยายที่เป็นอันตรายสามารถอ่านข้อมูลการเข้าสู่ระบบที่จัดเก็บไว้ หรือเรียกใช้โค้ดโดยอำเภอใจ \n \nการเชื่อถือใบรับรองนี้แสดงว่าคุณยอมรับความเสี่ยงเหล่านี้ เต็มหน้าจอ @@ -138,7 +138,7 @@ ไม่มีแอนิเมชั่น ปกติ เร็ว - ประเภทการหมุนเริ่มต้น + การหมุนเริ่มต้น อิสระ ล็อกแนวตั้ง ล็อกแนวนอน @@ -157,7 +157,7 @@ ตอนที่สี่ก่อนตอนที่อ่านล่าสุด ตอนที่ห้าก่อนตอนที่อ่านล่าสุด ดาวน์โหลดตอนใหม่ - บริการ + ตัวติดตาม สำรองข้อมูล สามารถใช้ในการเรียกคืนค่าคลังปัจจุบัน เรียกคืนค่าการสำรองข้อมูล @@ -325,7 +325,7 @@ แสดงตำแหน่งโซนการแตะ แหล่งที่มาจากส่วนขยายนี้อาจมีเนื้อหา NSFW (18+) 18+ - ส่วนขยายนี้ไม่ได้มาจากรายการส่วนขยายทางการของ Tachiyomi + ส่วนขยายนี้ไม่ได้มาจากรายการทางการ ส่วนขยายนี้ไม่สามารถใช้ได้อีกต่อไป แอปอาจทำงานไม่ถูกต้องและอาจทำให้เกิดปัญหากับแอปได้ ขอแนะนำให้ถอนการติดตั้งออก ไม่เป็นทางการ ล้าสมัย @@ -395,14 +395,12 @@ ตอนล่าสุด ติดตามแล้ว เมนู - กดกลับอีกครั้งเพื่อออก - ปลดล็อก Tachiyomi แหล่งที่มา เพิ่มเติม ช่องว่างด้านข้าง การอ่าน โหมดการอ่าน - ประเภทการหมุน + การหมุน โซนการแตะ หน้ายาวแยกช่องว่าง ขวา @@ -451,7 +449,6 @@ ค้นหา \"%1$s\" ในทั้งหมด ที่ตรึงไว้ ใช้ล่าสุด - ตรวจสอบเว็บไซต์ใน WebView แท็บ ตอนที่ดาวน์โหลดแล้ว จากคลัง @@ -499,7 +496,7 @@ แหล่งที่มาที่หายไป: แฟ้มสํารองข้อมูลไม่มีรายการคลังใด ๆ ข้อมูลสำรองไม่ถูกต้อง - ซิงค์ทางเดียวเพื่ออัปเดตความคืบหน้าของตอนกับบริการการติดตาม ตั้งค่าการติดตามรายการในแต่ละรายการได้จากปุ่มติดตาม + ซิงค์ทางเดียวเพื่ออัปเดตความคืบหน้าของตอนกับตัวติดตามภายนอก ตั้งค่าการติดตามรายการในแต่ละรายการได้จากปุ่มติดตาม รายการในหมวดหมู่ที่ยกเว้นไว้จะไม่ถูกดาวน์โหลดแม้ว่าจะอยู่ในหมวดหมู่ที่รวมอยู่ด้วยก็ตาม ดาวน์โหลดอัตโนมัติ กำลังตรวจสอบตอนหาใหม่ @@ -526,7 +523,7 @@ ความคืบหน้า อัปเดตการตั้งค่าตอนเริ่มต้นแล้ว โปรดอัปเดตแอพ WebView เพื่อความเข้ากันได้ที่ดีขึ้น - Tachiyomi จำเป็นต้องใช้ WebView + แอปจำเป็นต้องใช้ WebView เพื่อให้ทำงานได้ ไม่สามารถเลี่ยงผ่าน Cloudflare ได้ มีการอัปเดตส่วนขยาย %d รายการพร้อมใช้งาน @@ -569,7 +566,7 @@ %1$d วันที่ผ่านมา - บริการขั้นสูง + ตัวติดตามขั้นสูง ควรเก็บสำเนาของข้อมูลสำรองไว้ที่อื่นด้วยเช่นกัน บันทึกบันทึกอย่างละเอียดไปยังบันทึกของระบบ (ลดประสิทธิภาพของแอป) ช่วยแปล @@ -594,8 +591,8 @@ เร็วที่สุด เร็ว คู่มือการติดตาม - หมวดหมู่ที่ยกเว้น - บริการที่มีคุณลักษณะขั้นสูงสำหรับแหล่งที่มาเฉพาะ รายการจะถูกติดตามโดยอัตโนมัติเมื่อเพิ่มลงในคลัง + หมวดหมู่ที่ยกเว้นไว้ + ใช้สำหรับเฉพาะบางแหล่งที่มา รายการจะถูกติดตามโดยอัตโนมัติเมื่อเพิ่มลงในคลัง ข้อมูลแอพ เกิดข้อผิดพลาดในการรับรายการส่วนขยาย ล้างแคชตอนในเมื่อเปิดแอพ @@ -640,7 +637,6 @@ ปิด ล้างข้อมูล WebView ล้างข้อมูล WebView แล้ว - ขณะแบตเตอรี่ยังไม่ต่ำ ไม่พบแหล่งที่มาที่ติดตั้งแล้ว ไม่พบแหล่งที่มาใด ๆ จำนวนตอนที่ยังไม่ได้อ่าน @@ -800,5 +796,12 @@ แตะที่นี่เพื่อขอความช่วยเหลือเกี่ยวกับ Cloudflare ไม่สามารถสร้างไฟล์สำรองข้อมูล มีลิขสิทธิ์แล้ว - ไม่มีตอนให้แสดง - เข้าสู่ระบบการติดตาม + เข้าสู่ระบบตัวติดตาม + ปลดล็อก %s + ไม่มีการเชื่อมต่ออินเทอร์เน็ต + HTTP %d, ดูเว็บไซต์ใน WebView + ไม่สามารถเข้าถึง %s ได้ + ย้ายเรื่องไปด้านล่าง + ประทับเวลาแบบสัมพันธ์กัน + แสดง \"%1$s\" แทน \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-tr/strings.xml b/i18n/src/main/res/values-tr/strings.xml index 4739c4b25..f1f3d3e0c 100644 --- a/i18n/src/main/res/values-tr/strings.xml +++ b/i18n/src/main/res/values-tr/strings.xml @@ -99,7 +99,7 @@ Güvenilmeyen uzantı Bu uzantı güvenilmeyen bir sertifika ile imzalanmış ve etkinleştirilmedi. \n -\nKötü niyetli bir uzantı, Tachiyomi\'de saklanan her giriş kimlik bilgisini okuyabilir veya rastgele kod yürütebilir. +\nKötü niyetli bir uzantı, saklanan her giriş kimlik bilgisini okuyabilir veya rastgele kod yürütebilir. \n \nBu sertifikaya güvenerek bu riskleri kabul etmiş oluyorsunuz. Tam Ekran @@ -137,7 +137,7 @@ Animasyon yok Normal Hızlı - Ön tanımlı döndürme türü + Ön tanımlı döndürme Bağımsız Kilitli dikey Kilitli yatay @@ -328,7 +328,6 @@ Cloudflare baypas edilemedi Daha iyi uyumluluk için lütfen WebView\'i güncelleyin Bölüm güncellemeleri - Tachiyomi\'nin kilidi aç Güvenli ekran, uygulamalar arasında geçiş yaparken uygulama içeriğini gizler ve ekran görüntüsü alınmasını engeller Ekran @@ -362,7 +361,6 @@ Uzantı güncellemesi var %d uzantı güncellemesi var - Web sitesini WebView\'de görüntüle Uzantı güncellemeleri Kitaplık güncelleniyor Okunan @@ -380,9 +378,8 @@ Daha az Daha fazla Web sitesi - WebView, Tachiyomi için gereklidir + Web Görünümü, uygulamanın işlevselliği için gereklidir Açık kaynak lisansları - Çıkmak için yeniden geriye basın Yalnızca indirilen Böl. %1$s - %2$s Yedek geri yüklenemedi @@ -414,7 +411,7 @@ Bölüm ilerlemesini, izleme hizmetlerinde güncellemek için tek yönlü eşitleme. Her girdinin izleme düğmesinden, izlemeyi ayarlayın. Kitaplıktakilerin kapaklarını yenile - Bu uzantı resmi Tachiyomi uzantıları dizelgesinden değil. + Bu uzantı resmi dizelgeden değil. Resmi olmayan Yükleme tarihine göre Veri @@ -535,7 +532,7 @@ Panoya kopyalanamadı Yatay Dikey - Döndürme türü + Döndürme Girdilerin başlığına göre sıralaç oluşturur Sayfaları ayrı sıralaçlara kaydet Eylemler @@ -633,7 +630,7 @@ SSS ve Kılavuzlar Okunmayan bölümü olan Bu başlamadı - Manga bitirildiği için atlandı + Dizi bitirildiği için atlandı Okunmamış bölümler olduğu için atlandı Hiçbir bölüm okunmadığı için atlandı Geniş görüntüleri kaydır @@ -652,7 +649,6 @@ WebView verileri temizlendi WebView verilerini temizle Kapat - Pil düşük olmadığında Kurulu kaynak bulunamadı Kaynak bulunamadı Son güncelleme denetimi @@ -810,4 +806,16 @@ Güncelleme aralığını ayarla Sonuç var İndirilenleri sil + Yedekleme dosyası oluşturulamadı + Lisanslı - Gösterilecek bölüm yok + İndirilenler dizini geçersiz kılındı + Cloudflare\'le ilgili yardım için tıklayın + %s verisine erişilemedi + İzleme girişi + Kitaplık eşleşmesi tamamlandı + HTTP %d, siteyi Web Görünümünde denetle + Genel ağ bağlantısı yok + Kitaplık eşleştiriliyor + Aç: %s + Diziyi en alta taşı \ No newline at end of file diff --git a/i18n/src/main/res/values-uk/strings.xml b/i18n/src/main/res/values-uk/strings.xml index bf43f2527..863ca3c0c 100644 --- a/i18n/src/main/res/values-uk/strings.xml +++ b/i18n/src/main/res/values-uk/strings.xml @@ -99,7 +99,7 @@ Ненадійне розширення Це розширення було підписано ненадійним сертифікатом та не було активовано. \n -\nЗловмисне розширення может зчитувати будь-які облікові дані для входу, що зберігаються в Tachiyomi, або виконувати довільний код. +\nШкідливе розширення може зчитувати будь-які збережені облікові дані для входу або виконати довільний код. \n \nДовіряючи цьому сертифікату, ви приймаєте ці ризики. Повноекранний режим @@ -137,7 +137,7 @@ Без анімації Нормальна Швидка - Тип основної орієнтації + Орієнтація за замовчуванням Вільна Заблокована портретна Заблокована альбомна @@ -156,7 +156,7 @@ Четвертий від останнього прочитаного розділу П\'ятий від останнього прочитаного розділу Завантажувати нові розділи - Сервіси + Трекери Створити резервну копію Можна використовувати для відновлення поточної бібліотеки Відновити резервну копію @@ -330,7 +330,6 @@ Не вдалось обійти Cloudflare Будь ласка, оновіть WebView для кращої сумісності Оновлення розділів - Розблокувати Tachiyomi Ховати вміст застосунку при перемиканні застосунків та блокувати скріншоти Відображення @@ -372,7 +371,6 @@ %d наявних оновлень розширень Оновлення розширень - Відкрити сайт у WebView Читання Джерела Оновлення бібліотеки @@ -388,14 +386,13 @@ Додати до бібліотеки Менше Більше - WebView необхідний для Tachiyomi + WebView необхідний для роботи застосунку Ліцензії з відкритим кодом Сторінка - Натисніть ще раз, щоб вийти Тільки завантажені Сірий Зменшує смугастість, але може вплинути на продуктивність - Це розширення не входить до переліку офіційних розширень Tachiyomi. + Це розширення не з офіційного списку. Неофіційне %d категорія @@ -436,7 +433,7 @@ Відсутні джерела: Бекап не містить жодних записів бібліотеки. Невірний файл резервної копії - Одностороння синхронізація для оновлення прогресу розділу в службах стеження. Налаштуйте для окремих записів з допомогою кнопки \"Стежити\". + Одностороння синхронізація для оновлення прогресу розділу у зовнішніх сервісах стеження. Налаштуйте відстеження окремих записів за допомогою кнопки відстеження. Показувати вкладки категорій Зручна сітка Розд. %1$s - %2$s @@ -556,7 +553,7 @@ Не вдалось скопіювати до буферу обміну Альбомна Портретна - Тип орієнтації + Орієнтація Створювати теки в відповідності до назви записів Зберігати сторінки до окремих тек Дії @@ -588,8 +585,8 @@ У деяких виробників є додаткові обмеження застосунків, котрі вбивають фонові сервіси. На цьому сайті більше інформації з приводу того, як це виправити. Фонова активність Резервування/Відновлення можуть не працювати належним чином, у випадку, якщо вимкнено Оптимізацію MIUI. - Сервіси, які надають розширені можливості для певних джерел. Записи будуть автоматично відстежуватись при додаванні до бібліотеки. - Розширені сервіси + Надає розширені функції для конкретних джерел. Записи автоматично відстежуються при додаванні до бібліотеки. + Розширені трекери Найнижча Низька Висока @@ -693,7 +690,6 @@ Тицьніть задля подробиць Не вдається відкрити останній прочитаний розділ Мова застосунку - Коли батарею заряджено Версія Мова Вікові обмеження @@ -843,9 +839,16 @@ Має результати Синхронізацію бібліотеки завершено Синхронізація бібліотеки - Відстеження входу + Логін трекера Індекс завантажень недійсний Натисніть тут, щоб отримати допомогу з Cloudflare Не вдалося створити файл резервної копії Ліцензовано - немає розділів для показу + Розблокувати %s + Перемістити серію вниз + Немає Інтернет-з\'єднання + Http %d, перевірте вебсайт у WebView + Не вдалося досягти %s + Відносні позначки часу + \"%1$s\" замість \"%2$s\" \ No newline at end of file diff --git a/i18n/src/main/res/values-uz/strings.xml b/i18n/src/main/res/values-uz/strings.xml index 49cf51aef..9b2703dbe 100644 --- a/i18n/src/main/res/values-uz/strings.xml +++ b/i18n/src/main/res/values-uz/strings.xml @@ -35,8 +35,6 @@ Filtr Menyu Sozlamalar - Chiqish uchun orqaga tugmasini bosing - Tachiyomini ochish Tarix Kuzatish Boblar @@ -271,7 +269,6 @@ Til Portret Har kun - Quvvat past bo\'lmaganda Avtomatik yangilanish O\'chiq Kulrang diff --git a/i18n/src/main/res/values-vi/strings.xml b/i18n/src/main/res/values-vi/strings.xml index 8a904f98d..e37aebf1b 100644 --- a/i18n/src/main/res/values-vi/strings.xml +++ b/i18n/src/main/res/values-vi/strings.xml @@ -313,7 +313,7 @@ Sao chép Không có danh mục. Nhấn vào dấu cộng để tạo một nhóm từ thư viện. Thường gặp - Nhấn giữ để mở hộp thoại + Nhấn giữ để mở mục hành động Mở bằng WebView Màu 32-bit Bỏ qua chap đã đọc @@ -339,7 +339,6 @@ Kiểm tra các chương cập nhật mới Đang cập nhật thư viện Đã tạm ngưng - Kiểm tra trang web bằng WebView Bạn đã đăng xuất Đăng xuất tài khoản Đăng xuất khỏi %1$s\? @@ -387,7 +386,6 @@ Đảo ngược lựa chọn Chương mới nhất Mục chính - Mở Khoá Tachiyomi Nguồn Thêm Đồng bộ một chiều để cập nhật tiến trình chương trong các dịch vụ theo dõi. Thiết lập theo dõi cho các truyện riêng lẻ từ nút theo dõi. @@ -412,7 +410,6 @@ Tắt tất cả Bật tất cả Ngày thêm - Nhấn trở lại lần nữa để thoát Bản sao lưu không chứa truyện nào. Tệp sao lưu không hợp lệ Thêm vào thư viện @@ -509,7 +506,7 @@ Dữ liệu DNS qua HTTPS (DoH) Mạng - Quá trình phục hồi bị huỷ bỏ + Quá trình khôi phục đã bị ngắt Khôi phục thất bại Đang trong quá trình phục hồi Lưu trữ thất bại @@ -596,7 +593,7 @@ Chủ đề: Âm & Dương Chủ đề: Yotsuba Chế độ đen tuyền - Tùy chỉnh mỗi danh mục cho sắp xếp và hiện thị + Tùy chỉnh mỗi danh mục cho sắp xếp Hướng dẫn theo dõi Dịch vụ nâng cao Dịch vụ cung cấp các tính năng nâng cao cho các nguồn cụ thể. Truyện sẽ tự động theo dõi khi được thêm vào thư viện của bạn. @@ -655,7 +652,7 @@ Chưa bắt đầu đọc %1$d cập nhật được bỏ qua Nhấn để tìm hiểu thêm - Phóng ảnh phong cảnh + Phóng ảnh phong cảnh tự động Hiển thị truyện Xoay ảnh rộng Lưới chỉ mỗi bìa @@ -670,7 +667,6 @@ Một phiên bản mới sẵn sàng từ mục phát hành chính thức. Nhấn để học cách kết hợp các mục phát hành không chính thức từ F-Droid. Lỗi khi lưu ảnh Đóng - Khi pin không quá ít Không có nguồn đã cài tìm thấy Không có nguồn nào được tìm thấy Mở trên GitHub @@ -820,4 +816,5 @@ Cập nhật dự kiến tiếp theo Nhấn vào đây để được trợ giúp về Cloudflare Chưa công bố + Mở khoá %s \ No newline at end of file diff --git a/i18n/src/main/res/values-zh-rCN/strings.xml b/i18n/src/main/res/values-zh-rCN/strings.xml index 654ecf2b4..2a32f659f 100644 --- a/i18n/src/main/res/values-zh-rCN/strings.xml +++ b/i18n/src/main/res/values-zh-rCN/strings.xml @@ -324,7 +324,6 @@ 待更新 无法绕过 Cloudflare 请更新 WebView 应用以获得更好的兼容性 - 解锁 Tachiyomi 阻止截屏,并在切换后台时隐藏预览图 显示 章节更新 @@ -358,7 +357,6 @@ %d 个插件可更新 插件更新 - 在 WebView 中检查网站 图源 正在更新书架 阅读 @@ -375,7 +373,6 @@ 展开 开源许可证 官网 - 再按一次退出 添加到书架 在书架中 仅限已下载内容 @@ -639,7 +636,6 @@ 打开 GitHub 页面 已清除 WebView 数据 清除 WebView 数据 - 非低电量时 关闭 未找到已安装的图源 未找到图源 @@ -800,4 +796,8 @@ 已清除下载索引 登录进度记录平台 无法创建备份文件 + 已有正版,没有章节可供显示 + 未连接网络 + HTTP %d,请在 WebView 中检查网站 + 无法连接到 %s \ No newline at end of file diff --git a/i18n/src/main/res/values-zh-rTW/strings.xml b/i18n/src/main/res/values-zh-rTW/strings.xml index 3736b238a..9fa76f271 100644 --- a/i18n/src/main/res/values-zh-rTW/strings.xml +++ b/i18n/src/main/res/values-zh-rTW/strings.xml @@ -113,7 +113,7 @@ 自訂位置 已停用 自動下載新章節 - 服務 + 歷程平台 建立備份 還原備份 備份位置 @@ -262,11 +262,11 @@ 信任 不信任 不信任的擴充套件 - 這個擴充套件的憑證並不可靠,且尚未被啟用。 + 此擴充功能使用未受信任的憑證簽署,並未啟用。 \n -\n惡意的擴充套件可能存取任何儲存於 Tachiyomi 中的登錄憑證,或執行任意的程式碼。 +\n惡意擴充功能可能會讀取任何已儲存的登入憑證或執行任意程式碼。 \n -\n信任這個憑證,即表示你願意承擔上述風險。 +\n通過信任此憑證,您接受這些風險。 最後閱畢的章節 最大備份保留數 不在書櫃中的作品閱讀進度將被清除,你確定嗎? @@ -303,7 +303,6 @@ 深色模式 開啟 日期格式 - 再按一次以離開 將套用至你書櫃中的作品 僅限下載內容 開放原始碼授權 @@ -317,7 +316,6 @@ 永不 立即 閒置時鎖定 - 解除鎖定 Tachiyomi 上鎖應用程式 在切換應用程式時隱藏預覽,並禁止擷取螢幕畫面。 防窺畫面 @@ -347,7 +345,7 @@ 最近使用 本機來源入門指南 請更新 WebView 以獲得更佳的相容性 - WebView 為 Tachiyomi 的必要元件 + 應用程式運作需要 WebView 反向選擇 自動重新整理中繼資料 更新書櫃時一併檢查封面與簡介是否有更動 @@ -400,7 +398,7 @@ 共 %1$s 章 - 將章節進度單向同步至閱讀歷程平台,請逕行前往個別作品的「歷程」專區以設置。 + 將閱讀進度單向同步至外部歷程平台。請逕行前往個別作品的「歷程」專區以設置。 %d 個類別 @@ -417,7 +415,7 @@ 歷時 %1$s,出現 %2$s 個錯誤 %02d 分 %02d 秒 - 這個擴充套件不是由 Tachiyomi 所提供。 + 此擴充功能不在官方清單中。 非官方 第 %1$s 章 - %2$s @@ -470,7 +468,6 @@ 清除閱讀記錄 暫停閱讀記錄 無痕模式 - 在 WebView 中檢查網站 找不到檔案選擇器 《%1$s》:%2$s,第 %3$d 頁 來源遷移說明 @@ -555,8 +552,8 @@ 部分裝置製造商設有額外的應用程式限制來終止背景服務。此網站提供了修復這項問題的詳細資訊。 背景活動 若停用 MIUI 最佳化,備份與還原可能無法正確執行。 - 為特定來源提供增強功能的服務。當作品被加入書櫃時,將自動登錄閱讀歷程。 - 增強服務 + 為特定來源提供增強功能。當作品被加入書櫃時,將自動登錄閱讀歷程。 + 增強式歷程平台 歷程平台說明 獨立各類別的排序方式 純黑深色模式 @@ -586,7 +583,7 @@ 入門指南 新增 協助翻譯 - 排除的類別 + 不包括的漫畫類別 程式資訊 若要使用 Shizuku 來安裝擴充套件,請先安裝 Shizuku 並開啟之。 傳統 @@ -640,7 +637,6 @@ 清除 WebView 資料 已清除 WebView 資料 關閉 - 電量充足時 找不到來源 找不到已安裝的來源 上次檢查更新日期 @@ -801,4 +797,11 @@ 登入歷程平台 無法建立備份檔 已有正版,沒有章節可供顯示 + HTTP %d,請在 WebView 中檢查網站 + 沒有網際網路連線 + 無法連上 %s + 解鎖 %s + 置底此作品 + 相對時間戳記 + 以「%1$s」表示「%2$s」 \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 634051417..739bbc5d4 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -39,9 +39,8 @@ Local Downloaded - Unlock Tachiyomi + Unlock %s Authenticate to confirm change - Press back again to exit Settings @@ -94,6 +93,8 @@ Set categories Do you wish to delete the category \"%s\"? Delete category + Sort categories + Would you like to sort the categories alphabetically? Edit cover View chapters Pause @@ -140,6 +141,7 @@ Move to top Move series to top Move to bottom + Move series to bottom Install Share Save @@ -203,6 +205,9 @@ Yotsuba Tidal Wave Pure black dark mode + Relative timestamps + + \"%1$s\" instead of \"%2$s\" Date format Manage notifications @@ -304,9 +309,9 @@ Uninstall App info Untrusted extension - This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any login credentials stored in Tachiyomi or execute arbitrary code.\n\nBy trusting this certificate you accept these risks. + This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any stored login credentials or execute arbitrary code.\n\nBy trusting this certificate 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 Tachiyomi extensions list. + This extension is not from the official list. Failed to get extensions list Version Language @@ -331,7 +336,6 @@ If the placement of the split wide pages don\'t match reading direction Rotate wide pages to fit Flip orientation of rotated wide pages - Split tall images (BETA) Double tap to zoom Show content in cutout area Animate page transitions @@ -366,8 +370,6 @@ Both Actions Show actions on long tap - Save pages into separate folders - Creates folders according to entries\' title Background color White Gray @@ -407,8 +409,8 @@ No animation Normal Fast - Default rotation type - Rotation type + Default rotation + Rotation Free Portrait Reverse portrait @@ -461,13 +463,13 @@ Tracking guide Update progress after reading - Services - One-way sync to update the chapter progress in tracking services. Set up tracking for individual entries from their tracking button. - Enhanced services + Trackers + One-way sync to update the chapter progress in external tracker services. Set up tracking for individual entries from their tracking button. + Enhanced trackers Available but source not installed: %s - Services that provide enhanced features for specific sources. Entries are automatically tracked when added to your library. + Provides enhanced features for specific sources. Entries are automatically tracked when added to your library. Track - Tracking login + Tracker login Hide entries already in library @@ -496,6 +498,8 @@ Backup is already in progress What do you want to backup? + App settings + Source settings Creating backup Backup failed Storage permissions not granted @@ -506,7 +510,7 @@ Restoring backup Restoring backup failed Canceled restore - You should keep copies of backups in other places as well. + You should keep copies of backups in other places as well. Backups may contain sensitive data including any stored passwords; be careful if sharing. Syncing library @@ -872,7 +876,7 @@ Checking for new chapters - Updating library… (%1$d/%2$d) + Updating library… (%s) Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more. New chapters found @@ -941,7 +945,7 @@ Tap here for help with Cloudflare *required - WebView is required for Tachiyomi + WebView is required for the app to function Please update the WebView app for better compatibility Updated default chapter settings diff --git a/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/StartupBenchmark.kt b/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/StartupBenchmark.kt index cd1002770..87c1d33c1 100644 --- a/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/StartupBenchmark.kt +++ b/macrobenchmark/src/main/java/tachiyomi/macrobenchmark/StartupBenchmark.kt @@ -60,11 +60,16 @@ abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) { @Test fun startupBaselineProfileDisabled() = startup( - CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Disable, warmupIterations = 1), + CompilationMode.Partial( + baselineProfileMode = BaselineProfileMode.Disable, + warmupIterations = 1, + ), ) @Test - fun startupBaselineProfile() = startup(CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require)) + fun startupBaselineProfile() = startup( + CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require), + ) @Test fun startupFullCompilation() = startup(CompilationMode.Full()) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt index 87e17416a..eea05b4ca 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt @@ -182,7 +182,10 @@ fun AdaptiveSheet( shape = MaterialTheme.shapes.extraLarge, tonalElevation = tonalElevation, content = { - BackHandler(enabled = anchoredDraggableState.targetValue == 0, onBack = internalOnDismissRequest) + BackHandler( + enabled = anchoredDraggableState.targetValue == 0, + onBack = internalOnDismissRequest, + ) content() }, ) @@ -200,49 +203,50 @@ fun AdaptiveSheet( } } -private fun AnchoredDraggableState.preUpPostDownNestedScrollConnection() = object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.Drag) { - dispatchRawDelta(delta).toOffset() - } else { - Offset.Zero +private fun AnchoredDraggableState.preUpPostDownNestedScrollConnection() = + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.Drag) { + dispatchRawDelta(delta).toOffset() + } else { + Offset.Zero + } } - } - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - return if (source == NestedScrollSource.Drag) { - dispatchRawDelta(available.toFloat()).toOffset() - } else { - Offset.Zero + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + return if (source == NestedScrollSource.Drag) { + dispatchRawDelta(available.toFloat()).toOffset() + } else { + Offset.Zero + } } - } - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = available.toFloat() - return if (toFling < 0 && offset > anchors.minAnchor()) { - settle(toFling) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = available.toFloat() + return if (toFling < 0 && offset > anchors.minAnchor()) { + settle(toFling) + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + settle(velocity = available.toFloat()) + return available + } + + private fun Float.toOffset(): Offset = Offset(0f, this) + + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = this.y + + @JvmName("offsetToFloat") + private fun Offset.toFloat(): Float = this.y } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - settle(velocity = available.toFloat()) - return available - } - - private fun Float.toOffset(): Offset = Offset(0f, this) - - @JvmName("velocityToFloat") - private fun Velocity.toFloat() = this.y - - @JvmName("offsetToFloat") - private fun Offset.toFloat(): Float = this.y -} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/CircularProgressIndicator.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/CircularProgressIndicator.kt index dafe3237d..bda305890 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/CircularProgressIndicator.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/CircularProgressIndicator.kt @@ -37,9 +37,7 @@ import androidx.compose.ui.tooling.preview.Preview * By always rotating we give the feedback to the user that the application isn't 'stuck'. */ @Composable -fun CombinedCircularProgressIndicator( - progress: Float, -) { +fun CombinedCircularProgressIndicator(progress: Float) { val animatedProgress by animateFloatAsState( targetValue = progress, animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt index b70cb2705..ffd4b84e9 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt @@ -23,10 +23,7 @@ import androidx.compose.ui.unit.dp import tachiyomi.presentation.core.theme.header @Composable -fun CollapsibleBox( - heading: String, - content: @Composable () -> Unit, -) { +fun CollapsibleBox(heading: String, content: @Composable () -> Unit) { var expanded by remember { mutableStateOf(false) } Column { diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LinkIcon.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LinkIcon.kt index 17dc818ca..86d1225b5 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LinkIcon.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LinkIcon.kt @@ -11,12 +11,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp @Composable -fun LinkIcon( - modifier: Modifier = Modifier, - label: String, - icon: ImageVector, - url: String, -) { +fun LinkIcon(modifier: Modifier = Modifier, label: String, icon: ImageVector, url: String) { val uriHandler = LocalUriHandler.current IconButton( modifier = modifier.padding(4.dp), diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt index c0ffb35d4..2fe128746 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ListGroupHeader.kt @@ -9,10 +9,7 @@ import androidx.compose.ui.text.font.FontWeight import tachiyomi.presentation.core.components.material.padding @Composable -fun ListGroupHeader( - modifier: Modifier = Modifier, - text: String, -) { +fun ListGroupHeader(modifier: Modifier = Modifier, text: String) { Text( text = text, modifier = modifier diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt index f1d544db7..08bf2d567 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt @@ -12,6 +12,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material.ContentAlpha import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward @@ -42,6 +45,7 @@ import androidx.compose.ui.unit.dp import tachiyomi.core.preference.Preference import tachiyomi.core.preference.TriState import tachiyomi.core.preference.toggle +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.theme.header import tachiyomi.presentation.core.util.collectAsState @@ -51,16 +55,12 @@ object SettingsItemsPaddings { } @Composable -fun HeadingItem( - @StringRes labelRes: Int, -) { +fun HeadingItem(@StringRes labelRes: Int) { HeadingItem(stringResource(labelRes)) } @Composable -fun HeadingItem( - text: String, -) { +fun HeadingItem(text: String) { Text( text = text, style = MaterialTheme.typography.header, @@ -74,11 +74,7 @@ fun HeadingItem( } @Composable -fun IconItem( - label: String, - icon: ImageVector, - onClick: () -> Unit, -) { +fun IconItem(label: String, icon: ImageVector, onClick: () -> Unit) { BaseSettingsItem( label = label, widget = { @@ -93,11 +89,7 @@ fun IconItem( } @Composable -fun SortItem( - label: String, - sortDescending: Boolean?, - onClick: () -> Unit, -) { +fun SortItem(label: String, sortDescending: Boolean?, onClick: () -> Unit) { val arrowIcon = when (sortDescending) { true -> Icons.Default.ArrowDownward false -> Icons.Default.ArrowUpward @@ -122,10 +114,7 @@ fun SortItem( } @Composable -fun CheckboxItem( - label: String, - pref: Preference, -) { +fun CheckboxItem(label: String, pref: Preference) { val checked by pref.collectAsState() CheckboxItem( label = label, @@ -135,11 +124,7 @@ fun CheckboxItem( } @Composable -fun CheckboxItem( - label: String, - checked: Boolean, - onClick: () -> Unit, -) { +fun CheckboxItem(label: String, checked: Boolean, onClick: () -> Unit) { BaseSettingsItem( label = label, widget = { @@ -153,11 +138,7 @@ fun CheckboxItem( } @Composable -fun RadioItem( - label: String, - selected: Boolean, - onClick: () -> Unit, -) { +fun RadioItem(label: String, selected: Boolean, onClick: () -> Unit) { BaseSettingsItem( label = label, widget = { @@ -285,7 +266,7 @@ fun TriStateItem( vertical = SettingsItemsPaddings.Vertical, ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(24.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.large), ) { val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled @@ -314,11 +295,7 @@ fun TriStateItem( } @Composable -fun TextItem( - label: String, - value: String, - onChange: (String) -> Unit, -) { +fun TextItem(label: String, value: String, onChange: (String) -> Unit) { OutlinedTextField( modifier = Modifier .fillMaxWidth() @@ -331,10 +308,7 @@ fun TextItem( } @Composable -fun SettingsChipRow( - @StringRes labelRes: Int, - content: @Composable FlowRowScope.() -> Unit, -) { +fun SettingsChipRow(@StringRes labelRes: Int, content: @Composable FlowRowScope.() -> Unit) { Column { HeadingItem(labelRes) FlowRow( @@ -344,7 +318,25 @@ fun SettingsChipRow( end = SettingsItemsPaddings.Horizontal, bottom = SettingsItemsPaddings.Vertical, ), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + content = content, + ) + } +} + +@Composable +fun SettingsIconGrid(@StringRes labelRes: Int, content: LazyGridScope.() -> Unit) { + Column { + HeadingItem(labelRes) + LazyVerticalGrid( + columns = GridCells.Adaptive(128.dp), + modifier = Modifier.padding( + start = SettingsItemsPaddings.Horizontal, + end = SettingsItemsPaddings.Horizontal, + bottom = SettingsItemsPaddings.Vertical, + ), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), content = content, ) } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt index c65aa9606..4f44f60e2 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/VerticalFastScroller.kt @@ -197,7 +197,8 @@ private fun rememberColumnWidthSums( horizontalArrangement, contentPadding, ) { - { constraints -> + { + constraints -> require(constraints.maxWidth != Constraints.Infinity) { "LazyVerticalGrid's width should be bound by parent" } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt index 9bbce88bf..3db94c018 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/AlertDialog.kt @@ -30,7 +30,9 @@ fun AlertDialogContent( title = title, content = { Column { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant, + ) { val textStyle = MaterialTheme.typography.bodyMedium ProvideTextStyle(textStyle) { Box( @@ -54,7 +56,9 @@ fun AlertDialogContent( ) .align(Alignment.End), ) { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.primary, + ) { val textStyle = MaterialTheme.typography.labelLarge ProvideTextStyle(value = textStyle, content = buttons) } @@ -86,7 +90,9 @@ fun AlertDialogContent( .fillMaxWidth(), ) { icon?.let { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.secondary, + ) { Box( Modifier .padding(IconPadding) @@ -97,7 +103,9 @@ fun AlertDialogContent( } } title?.let { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onSurface, + ) { val textStyle = MaterialTheme.typography.headlineSmall ProvideTextStyle(textStyle) { Box( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Button.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Button.kt index 3a5de0275..6a645ed6a 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Button.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Button.kt @@ -64,20 +64,19 @@ fun TextButton( ), contentPadding: PaddingValues = M3ButtonDefaults.TextButtonContentPadding, content: @Composable RowScope.() -> Unit, -) = - Button( - onClick = onClick, - modifier = modifier, - onLongClick = onLongClick, - enabled = enabled, - interactionSource = interactionSource, - elevation = elevation, - shape = shape, - border = border, - colors = colors, - contentPadding = contentPadding, - content = content, - ) +) = Button( + onClick = onClick, + modifier = modifier, + onLongClick = onLongClick, + enabled = enabled, + interactionSource = interactionSource, + elevation = elevation, + shape = shape, + border = border, + colors = colors, + contentPadding = contentPadding, + content = content, +) /** * Button with additional onLongClick functionality. diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconButton.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconButtonTokens.kt similarity index 100% rename from presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconButton.kt rename to presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconButtonTokens.kt diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconToggleButton.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconToggleButton.kt new file mode 100644 index 000000000..2abcc4b39 --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/IconToggleButton.kt @@ -0,0 +1,52 @@ +package tachiyomi.presentation.core.components.material + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.FilledIconToggleButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +@Composable +fun IconToggleButton( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + imageVector: ImageVector, + title: String, +) { + FilledIconToggleButton( + checked = checked, + onCheckedChange = onCheckedChange, + modifier = modifier + .height(48.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(MaterialTheme.padding.small), + ) { + Icon( + imageVector = imageVector, + contentDescription = null, + ) + + Text( + text = title, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt index 02e670ca7..39c00895b 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/NavigationRail.kt @@ -48,7 +48,10 @@ fun NavigationRail( .padding(vertical = MaterialTheme.padding.tiny) .selectableGroup(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny, alignment = Alignment.CenterVertically), + verticalArrangement = Arrangement.spacedBy( + MaterialTheme.padding.tiny, + alignment = Alignment.CenterVertically, + ), ) { if (header != null) { header() diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt index 7ecd5964c..cd14e2c0e 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Scaffold.kt @@ -99,7 +99,9 @@ import kotlin.math.max @Composable fun Scaffold( modifier: Modifier = Modifier, - topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), + topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior( + rememberTopAppBarState(), + ), topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {}, bottomBar: @Composable () -> Unit = {}, startBar: @Composable () -> Unit = {}, @@ -116,7 +118,11 @@ fun Scaffold( androidx.compose.material3.Surface( modifier = Modifier .nestedScroll(topBarScrollBehavior.nestedScrollConnection) - .onConsumedWindowInsetsChanged { remainingWindowInsets.insets = contentWindowInsets.exclude(it) } + .onConsumedWindowInsetsChanged { + remainingWindowInsets.insets = contentWindowInsets.exclude( + it, + ) + } .then(modifier), color = containerColor, contentColor = contentColor, @@ -271,7 +277,10 @@ private fun ScaffoldLayout( } else { max(bottomBarHeightPx.toDp(), fabOffsetDp) }, - start = max(insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection), startBarWidth.toDp()), + start = max( + insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection), + startBarWidth.toDp(), + ), end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection), ) content(innerPadding) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Surface.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Surface.kt index eda45695f..71866b61d 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Surface.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Surface.kt @@ -104,9 +104,7 @@ private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color { } } -private fun ColorScheme.surfaceColorAtElevation( - elevation: Dp, -): Color { +private fun ColorScheme.surfaceColorAtElevation(elevation: Dp): Color { if (elevation == 0.dp) return surface val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f return surfaceTint.copy(alpha = alpha).compositeOver(surface) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt index d547ad8d0..66e44ca24 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt @@ -46,10 +46,7 @@ private fun Modifier.tabIndicatorOffset( } @Composable -fun TabIndicator( - currentTabPosition: TabPosition, - currentPageOffsetFraction: Float, -) { +fun TabIndicator(currentTabPosition: TabPosition, currentPageOffsetFraction: Float) { SecondaryIndicator( modifier = Modifier .tabIndicatorOffset(currentTabPosition, currentPageOffsetFraction) @@ -59,10 +56,7 @@ fun TabIndicator( } @Composable -fun TabText( - text: String, - badgeCount: Int? = null, -) { +fun TabText(text: String, badgeCount: Int? = null) { val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f Row( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/CustomIcons.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/CustomIcons.kt new file mode 100644 index 000000000..8c8661b85 --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/CustomIcons.kt @@ -0,0 +1,7 @@ +package tachiyomi.presentation.core.icons + +/** + * Icons imported from https://simpleicons.org using + * https://github.com/DevSrSouza/svg-to-compose + */ +object CustomIcons diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Discord.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Discord.kt new file mode 100644 index 000000000..f9c80ff8e --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Discord.kt @@ -0,0 +1,77 @@ +package tachiyomi.presentation.core.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val CustomIcons.Discord: ImageVector + get() { + if (_discord != null) { + return _discord!! + } + _discord = Builder( + name = "Discord", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, + viewportWidth = 24.0f, viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(20.317f, 4.3698f) + arcToRelative(19.7913f, 19.7913f, 0.0f, false, false, -4.8851f, -1.5152f) + arcToRelative(0.0741f, 0.0741f, 0.0f, false, false, -0.0785f, 0.0371f) + curveToRelative(-0.211f, 0.3753f, -0.4447f, 0.8648f, -0.6083f, 1.2495f) + curveToRelative(-1.8447f, -0.2762f, -3.68f, -0.2762f, -5.4868f, 0.0f) + curveToRelative(-0.1636f, -0.3933f, -0.4058f, -0.8742f, -0.6177f, -1.2495f) + arcToRelative(0.077f, 0.077f, 0.0f, false, false, -0.0785f, -0.037f) + arcToRelative(19.7363f, 19.7363f, 0.0f, false, false, -4.8852f, 1.515f) + arcToRelative(0.0699f, 0.0699f, 0.0f, false, false, -0.0321f, 0.0277f) + curveTo(0.5334f, 9.0458f, -0.319f, 13.5799f, 0.0992f, 18.0578f) + arcToRelative(0.0824f, 0.0824f, 0.0f, false, false, 0.0312f, 0.0561f) + curveToRelative(2.0528f, 1.5076f, 4.0413f, 2.4228f, 5.9929f, 3.0294f) + arcToRelative(0.0777f, 0.0777f, 0.0f, false, false, 0.0842f, -0.0276f) + curveToRelative(0.4616f, -0.6304f, 0.8731f, -1.2952f, 1.226f, -1.9942f) + arcToRelative(0.076f, 0.076f, 0.0f, false, false, -0.0416f, -0.1057f) + curveToRelative(-0.6528f, -0.2476f, -1.2743f, -0.5495f, -1.8722f, -0.8923f) + arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0076f, -0.1277f) + curveToRelative(0.1258f, -0.0943f, 0.2517f, -0.1923f, 0.3718f, -0.2914f) + arcToRelative(0.0743f, 0.0743f, 0.0f, false, true, 0.0776f, -0.0105f) + curveToRelative(3.9278f, 1.7933f, 8.18f, 1.7933f, 12.0614f, 0.0f) + arcToRelative(0.0739f, 0.0739f, 0.0f, false, true, 0.0785f, 0.0095f) + curveToRelative(0.1202f, 0.099f, 0.246f, 0.1981f, 0.3728f, 0.2924f) + arcToRelative(0.077f, 0.077f, 0.0f, false, true, -0.0066f, 0.1276f) + arcToRelative(12.2986f, 12.2986f, 0.0f, false, true, -1.873f, 0.8914f) + arcToRelative(0.0766f, 0.0766f, 0.0f, false, false, -0.0407f, 0.1067f) + curveToRelative(0.3604f, 0.698f, 0.7719f, 1.3628f, 1.225f, 1.9932f) + arcToRelative(0.076f, 0.076f, 0.0f, false, false, 0.0842f, 0.0286f) + curveToRelative(1.961f, -0.6067f, 3.9495f, -1.5219f, 6.0023f, -3.0294f) + arcToRelative(0.077f, 0.077f, 0.0f, false, false, 0.0313f, -0.0552f) + curveToRelative(0.5004f, -5.177f, -0.8382f, -9.6739f, -3.5485f, -13.6604f) + arcToRelative(0.061f, 0.061f, 0.0f, false, false, -0.0312f, -0.0286f) + close() + moveTo(8.02f, 15.3312f) + curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f) + curveToRelative(0.0f, -1.3332f, 0.9555f, -2.4189f, 2.157f, -2.4189f) + curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f) + curveToRelative(0.0f, 1.3332f, -0.9555f, 2.4189f, -2.1569f, 2.4189f) + close() + moveTo(15.9948f, 15.3312f) + curveToRelative(-1.1825f, 0.0f, -2.1569f, -1.0857f, -2.1569f, -2.419f) + curveToRelative(0.0f, -1.3332f, 0.9554f, -2.4189f, 2.1569f, -2.4189f) + curveToRelative(1.2108f, 0.0f, 2.1757f, 1.0952f, 2.1568f, 2.419f) + curveToRelative(0.0f, 1.3332f, -0.946f, 2.4189f, -2.1568f, 2.4189f) + close() + } + } + .build() + return _discord!! + } + +private var _discord: ImageVector? = null diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Facebook.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Facebook.kt new file mode 100644 index 000000000..1bb4bda34 --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Facebook.kt @@ -0,0 +1,54 @@ +package tachiyomi.presentation.core.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val CustomIcons.Facebook: ImageVector + get() { + if (_facebook != null) { + return _facebook!! + } + _facebook = Builder( + name = "Facebook", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, + viewportWidth = 24.0f, viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(24.0f, 12.073f) + curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f) + reflectiveCurveToRelative(-12.0f, 5.373f, -12.0f, 12.0f) + curveToRelative(0.0f, 5.99f, 4.388f, 10.954f, 10.125f, 11.854f) + verticalLineToRelative(-8.385f) + horizontalLineTo(7.078f) + verticalLineToRelative(-3.47f) + horizontalLineToRelative(3.047f) + verticalLineTo(9.43f) + curveToRelative(0.0f, -3.007f, 1.792f, -4.669f, 4.533f, -4.669f) + curveToRelative(1.312f, 0.0f, 2.686f, 0.235f, 2.686f, 0.235f) + verticalLineToRelative(2.953f) + horizontalLineTo(15.83f) + curveToRelative(-1.491f, 0.0f, -1.956f, 0.925f, -1.956f, 1.874f) + verticalLineToRelative(2.25f) + horizontalLineToRelative(3.328f) + lineToRelative(-0.532f, 3.47f) + horizontalLineToRelative(-2.796f) + verticalLineToRelative(8.385f) + curveTo(19.612f, 23.027f, 24.0f, 18.062f, 24.0f, 12.073f) + close() + } + } + .build() + return _facebook!! + } + +private var _facebook: ImageVector? = null diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Github.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Github.kt new file mode 100644 index 000000000..25d16b5b9 --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Github.kt @@ -0,0 +1,59 @@ +package tachiyomi.presentation.core.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val CustomIcons.Github: ImageVector + get() { + if (_github != null) { + return _github!! + } + _github = Builder( + name = "Github", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, + viewportWidth = 24.0f, viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(12.0f, 0.297f) + curveToRelative(-6.63f, 0.0f, -12.0f, 5.373f, -12.0f, 12.0f) + curveToRelative(0.0f, 5.303f, 3.438f, 9.8f, 8.205f, 11.385f) + curveToRelative(0.6f, 0.113f, 0.82f, -0.258f, 0.82f, -0.577f) + curveToRelative(0.0f, -0.285f, -0.01f, -1.04f, -0.015f, -2.04f) + curveToRelative(-3.338f, 0.724f, -4.042f, -1.61f, -4.042f, -1.61f) + curveTo(4.422f, 18.07f, 3.633f, 17.7f, 3.633f, 17.7f) + curveToRelative(-1.087f, -0.744f, 0.084f, -0.729f, 0.084f, -0.729f) + curveToRelative(1.205f, 0.084f, 1.838f, 1.236f, 1.838f, 1.236f) + curveToRelative(1.07f, 1.835f, 2.809f, 1.305f, 3.495f, 0.998f) + curveToRelative(0.108f, -0.776f, 0.417f, -1.305f, 0.76f, -1.605f) + curveToRelative(-2.665f, -0.3f, -5.466f, -1.332f, -5.466f, -5.93f) + curveToRelative(0.0f, -1.31f, 0.465f, -2.38f, 1.235f, -3.22f) + curveToRelative(-0.135f, -0.303f, -0.54f, -1.523f, 0.105f, -3.176f) + curveToRelative(0.0f, 0.0f, 1.005f, -0.322f, 3.3f, 1.23f) + curveToRelative(0.96f, -0.267f, 1.98f, -0.399f, 3.0f, -0.405f) + curveToRelative(1.02f, 0.006f, 2.04f, 0.138f, 3.0f, 0.405f) + curveToRelative(2.28f, -1.552f, 3.285f, -1.23f, 3.285f, -1.23f) + curveToRelative(0.645f, 1.653f, 0.24f, 2.873f, 0.12f, 3.176f) + curveToRelative(0.765f, 0.84f, 1.23f, 1.91f, 1.23f, 3.22f) + curveToRelative(0.0f, 4.61f, -2.805f, 5.625f, -5.475f, 5.92f) + curveToRelative(0.42f, 0.36f, 0.81f, 1.096f, 0.81f, 2.22f) + curveToRelative(0.0f, 1.606f, -0.015f, 2.896f, -0.015f, 3.286f) + curveToRelative(0.0f, 0.315f, 0.21f, 0.69f, 0.825f, 0.57f) + curveTo(20.565f, 22.092f, 24.0f, 17.592f, 24.0f, 12.297f) + curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f) + } + } + .build() + return _github!! + } + +private var _github: ImageVector? = null diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Reddit.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Reddit.kt new file mode 100644 index 000000000..efed17f01 --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Reddit.kt @@ -0,0 +1,85 @@ +package tachiyomi.presentation.core.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val CustomIcons.Reddit: ImageVector + get() { + if (_reddit != null) { + return _reddit!! + } + _reddit = Builder( + name = "Reddit", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, + viewportWidth = 24.0f, viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(12.0f, 0.0f) + arcTo(12.0f, 12.0f, 0.0f, false, false, 0.0f, 12.0f) + arcToRelative(12.0f, 12.0f, 0.0f, false, false, 12.0f, 12.0f) + arcToRelative(12.0f, 12.0f, 0.0f, false, false, 12.0f, -12.0f) + arcTo(12.0f, 12.0f, 0.0f, false, false, 12.0f, 0.0f) + close() + moveTo(17.01f, 4.744f) + curveToRelative(0.688f, 0.0f, 1.25f, 0.561f, 1.25f, 1.249f) + arcToRelative(1.25f, 1.25f, 0.0f, false, true, -2.498f, 0.056f) + lineToRelative(-2.597f, -0.547f) + lineToRelative(-0.8f, 3.747f) + curveToRelative(1.824f, 0.07f, 3.48f, 0.632f, 4.674f, 1.488f) + curveToRelative(0.308f, -0.309f, 0.73f, -0.491f, 1.207f, -0.491f) + curveToRelative(0.968f, 0.0f, 1.754f, 0.786f, 1.754f, 1.754f) + curveToRelative(0.0f, 0.716f, -0.435f, 1.333f, -1.01f, 1.614f) + arcToRelative(3.111f, 3.111f, 0.0f, false, true, 0.042f, 0.52f) + curveToRelative(0.0f, 2.694f, -3.13f, 4.87f, -7.004f, 4.87f) + curveToRelative(-3.874f, 0.0f, -7.004f, -2.176f, -7.004f, -4.87f) + curveToRelative(0.0f, -0.183f, 0.015f, -0.366f, 0.043f, -0.534f) + arcTo(1.748f, 1.748f, 0.0f, false, true, 4.028f, 12.0f) + curveToRelative(0.0f, -0.968f, 0.786f, -1.754f, 1.754f, -1.754f) + curveToRelative(0.463f, 0.0f, 0.898f, 0.196f, 1.207f, 0.49f) + curveToRelative(1.207f, -0.883f, 2.878f, -1.43f, 4.744f, -1.487f) + lineToRelative(0.885f, -4.182f) + arcToRelative(0.342f, 0.342f, 0.0f, false, true, 0.14f, -0.197f) + arcToRelative(0.35f, 0.35f, 0.0f, false, true, 0.238f, -0.042f) + lineToRelative(2.906f, 0.617f) + arcToRelative(1.214f, 1.214f, 0.0f, false, true, 1.108f, -0.701f) + close() + moveTo(9.25f, 12.0f) + curveTo(8.561f, 12.0f, 8.0f, 12.562f, 8.0f, 13.25f) + curveToRelative(0.0f, 0.687f, 0.561f, 1.248f, 1.25f, 1.248f) + curveToRelative(0.687f, 0.0f, 1.248f, -0.561f, 1.248f, -1.249f) + curveToRelative(0.0f, -0.688f, -0.561f, -1.249f, -1.249f, -1.249f) + close() + moveTo(14.75f, 12.0f) + curveToRelative(-0.687f, 0.0f, -1.248f, 0.561f, -1.248f, 1.25f) + curveToRelative(0.0f, 0.687f, 0.561f, 1.248f, 1.249f, 1.248f) + curveToRelative(0.688f, 0.0f, 1.249f, -0.561f, 1.249f, -1.249f) + curveToRelative(0.0f, -0.687f, -0.562f, -1.249f, -1.25f, -1.249f) + close() + moveTo(9.284f, 15.99f) + arcToRelative(0.327f, 0.327f, 0.0f, false, false, -0.231f, 0.094f) + arcToRelative(0.33f, 0.33f, 0.0f, false, false, 0.0f, 0.463f) + curveToRelative(0.842f, 0.842f, 2.484f, 0.913f, 2.961f, 0.913f) + curveToRelative(0.477f, 0.0f, 2.105f, -0.056f, 2.961f, -0.913f) + arcToRelative(0.361f, 0.361f, 0.0f, false, false, 0.029f, -0.463f) + arcToRelative(0.33f, 0.33f, 0.0f, false, false, -0.464f, 0.0f) + curveToRelative(-0.547f, 0.533f, -1.684f, 0.73f, -2.512f, 0.73f) + curveToRelative(-0.828f, 0.0f, -1.979f, -0.196f, -2.512f, -0.73f) + arcToRelative(0.326f, 0.326f, 0.0f, false, false, -0.232f, -0.095f) + close() + } + } + .build() + return _reddit!! + } + +private var _reddit: ImageVector? = null diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/X.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/X.kt new file mode 100644 index 000000000..054030d03 --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/X.kt @@ -0,0 +1,53 @@ +package tachiyomi.presentation.core.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val CustomIcons.X: ImageVector + get() { + if (_x != null) { + return _x!! + } + _x = Builder( + name = "X", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, + viewportWidth = + 24.0f, + viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(18.901f, 1.153f) + horizontalLineToRelative(3.68f) + lineToRelative(-8.04f, 9.19f) + lineTo(24.0f, 22.846f) + horizontalLineToRelative(-7.406f) + lineToRelative(-5.8f, -7.584f) + lineToRelative(-6.638f, 7.584f) + horizontalLineTo(0.474f) + lineToRelative(8.6f, -9.83f) + lineTo(0.0f, 1.154f) + horizontalLineToRelative(7.594f) + lineToRelative(5.243f, 6.932f) + close() + moveTo(17.61f, 20.644f) + horizontalLineToRelative(2.039f) + lineTo(6.486f, 3.24f) + horizontalLineTo(4.298f) + close() + } + } + .build() + return _x!! + } + +private var _x: ImageVector? = null diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Preview.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/ThemePreviews.kt similarity index 100% rename from presentation-core/src/main/java/tachiyomi/presentation/core/util/Preview.kt rename to presentation-core/src/main/java/tachiyomi/presentation/core/util/ThemePreviews.kt diff --git a/presentation-widget/src/main/AndroidManifest.xml b/presentation-widget/src/main/AndroidManifest.xml index 568741e54..38943f13e 100644 --- a/presentation-widget/src/main/AndroidManifest.xml +++ b/presentation-widget/src/main/AndroidManifest.xml @@ -1,2 +1,39 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt new file mode 100644 index 000000000..26e1cd918 --- /dev/null +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt @@ -0,0 +1,153 @@ +package tachiyomi.presentation.widget + +import android.app.Application +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.Dp +import androidx.core.graphics.drawable.toBitmap +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.ImageProvider +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.SizeMode +import androidx.glance.appwidget.appWidgetBackground +import androidx.glance.appwidget.provideContent +import androidx.glance.background +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.padding +import androidx.glance.unit.ColorProvider +import coil.executeBlocking +import coil.imageLoader +import coil.request.CachePolicy +import coil.request.ImageRequest +import coil.size.Precision +import coil.size.Scale +import coil.transform.RoundedCornersTransformation +import eu.kanade.tachiyomi.core.security.SecurityPreferences +import eu.kanade.tachiyomi.util.system.dpToPx +import kotlinx.coroutines.flow.map +import tachiyomi.core.util.lang.withIOContext +import tachiyomi.domain.manga.model.MangaCover +import tachiyomi.domain.updates.interactor.GetUpdates +import tachiyomi.domain.updates.model.UpdatesWithRelations +import tachiyomi.presentation.widget.components.CoverHeight +import tachiyomi.presentation.widget.components.CoverWidth +import tachiyomi.presentation.widget.components.LockedWidget +import tachiyomi.presentation.widget.components.UpdatesWidget +import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius +import tachiyomi.presentation.widget.util.calculateRowAndColumnCount +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.Calendar +import java.util.Date + +abstract class BaseUpdatesGridGlanceWidget( + private val context: Context = Injekt.get(), + private val getUpdates: GetUpdates = Injekt.get(), + private val preferences: SecurityPreferences = Injekt.get(), +) : GlanceAppWidget() { + + override val sizeMode = SizeMode.Exact + + abstract val foreground: ColorProvider + abstract val background: ImageProvider + abstract val topPadding: Dp + abstract val bottomPadding: Dp + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val locked = preferences.useAuthenticator().get() + val containerModifier = GlanceModifier + .fillMaxSize() + .background(background) + .appWidgetBackground() + .padding(top = topPadding, bottom = bottomPadding) + .appWidgetBackgroundRadius() + + val manager = GlanceAppWidgetManager(context) + val ids = manager.getGlanceIds(javaClass) + val (rowCount, columnCount) = ids + .flatMap { manager.getAppWidgetSizes(it) } + .maxBy { it.height.value * it.width.value } + .calculateRowAndColumnCount(topPadding, bottomPadding) + + provideContent { + // If app lock enabled, don't do anything + if (locked) { + LockedWidget( + foreground = foreground, + modifier = containerModifier, + ) + return@provideContent + } + + val flow = remember { + getUpdates + .subscribe(false, DateLimit.timeInMillis) + .map { rawData -> + rawData.prepareData(rowCount, columnCount) + } + } + val data by flow.collectAsState(initial = null) + UpdatesWidget( + data = data, + modifier = containerModifier, + contentColor = foreground, + topPadding = topPadding, + bottomPadding = bottomPadding, + ) + } + } + + private suspend fun List.prepareData( + rowCount: Int, + columnCount: Int, + ): List> { + // Resize to cover size + val widthPx = CoverWidth.value.toInt().dpToPx + val heightPx = CoverHeight.value.toInt().dpToPx + val roundPx = context.resources.getDimension(R.dimen.appwidget_inner_radius) + return withIOContext { + this@prepareData + .distinctBy { it.mangaId } + .take(rowCount * columnCount) + .map { updatesView -> + val request = ImageRequest.Builder(context) + .data( + MangaCover( + mangaId = updatesView.mangaId, + sourceId = updatesView.sourceId, + isMangaFavorite = true, + url = updatesView.coverData.url, + lastModified = updatesView.coverData.lastModified, + ), + ) + .memoryCachePolicy(CachePolicy.DISABLED) + .precision(Precision.EXACT) + .size(widthPx, heightPx) + .scale(Scale.FILL) + .let { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + it.transformations(RoundedCornersTransformation(roundPx)) + } else { + it // Handled by system + } + } + .build() + Pair(updatesView.mangaId, context.imageLoader.executeBlocking(request).drawable?.toBitmap()) + } + } + } + + companion object { + val DateLimit: Calendar + get() = Calendar.getInstance().apply { + time = Date() + add(Calendar.MONTH, -3) + } + } +} diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/TachiyomiWidgetManager.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/TachiyomiWidgetManager.kt index e51b0337d..8227c6c51 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/TachiyomiWidgetManager.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/TachiyomiWidgetManager.kt @@ -19,7 +19,7 @@ class TachiyomiWidgetManager( fun Context.init(scope: LifecycleCoroutineScope) { combine( - getUpdates.subscribe(read = false, after = UpdatesGridGlanceWidget.DateLimit.timeInMillis), + getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.timeInMillis), securityPreferences.useAuthenticator().changes(), transform = { a, _ -> a }, ) @@ -27,6 +27,7 @@ class TachiyomiWidgetManager( .onEach { try { UpdatesGridGlanceWidget().updateAll(this) + UpdatesGridCoverScreenGlanceWidget().updateAll(this) } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to update widget" } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridCoverScreenGlanceReceiver.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridCoverScreenGlanceReceiver.kt new file mode 100644 index 000000000..721c6a3ac --- /dev/null +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridCoverScreenGlanceReceiver.kt @@ -0,0 +1,9 @@ +package tachiyomi.presentation.widget + +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver + +class UpdatesGridCoverScreenGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget + get() = UpdatesGridCoverScreenGlanceWidget() +} diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridCoverScreenGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridCoverScreenGlanceWidget.kt new file mode 100644 index 000000000..efb67f1c3 --- /dev/null +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridCoverScreenGlanceWidget.kt @@ -0,0 +1,13 @@ +package tachiyomi.presentation.widget + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.glance.ImageProvider +import androidx.glance.unit.ColorProvider + +class UpdatesGridCoverScreenGlanceWidget : BaseUpdatesGridGlanceWidget() { + override val foreground = ColorProvider(Color.White) + override val background = ImageProvider(R.drawable.appwidget_coverscreen_background) + override val topPadding = 0.dp + override val bottomPadding = 24.dp +} diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceReceiver.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceReceiver.kt index 93a4cf735..3ee232781 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceReceiver.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceReceiver.kt @@ -4,5 +4,6 @@ import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver class UpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget = UpdatesGridGlanceWidget() + override val glanceAppWidget: GlanceAppWidget + get() = UpdatesGridGlanceWidget() } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt index 9d7794b56..2de89ecce 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt @@ -1,134 +1,12 @@ package tachiyomi.presentation.widget -import android.app.Application -import android.content.Context -import android.graphics.Bitmap -import android.os.Build -import androidx.core.graphics.drawable.toBitmap -import androidx.glance.GlanceId -import androidx.glance.GlanceModifier +import androidx.compose.ui.unit.dp import androidx.glance.ImageProvider -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager -import androidx.glance.appwidget.SizeMode -import androidx.glance.appwidget.appWidgetBackground -import androidx.glance.appwidget.provideContent -import androidx.glance.background -import androidx.glance.layout.fillMaxSize -import coil.executeBlocking -import coil.imageLoader -import coil.request.CachePolicy -import coil.request.ImageRequest -import coil.size.Precision -import coil.size.Scale -import coil.transform.RoundedCornersTransformation -import eu.kanade.tachiyomi.core.security.SecurityPreferences -import eu.kanade.tachiyomi.util.system.dpToPx -import tachiyomi.core.util.lang.withIOContext -import tachiyomi.domain.manga.model.MangaCover -import tachiyomi.domain.updates.interactor.GetUpdates -import tachiyomi.domain.updates.model.UpdatesWithRelations -import tachiyomi.presentation.widget.components.CoverHeight -import tachiyomi.presentation.widget.components.CoverWidth -import tachiyomi.presentation.widget.components.LockedWidget -import tachiyomi.presentation.widget.components.UpdatesWidget -import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius -import tachiyomi.presentation.widget.util.calculateRowAndColumnCount -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -import java.util.Calendar -import java.util.Date +import androidx.glance.unit.ColorProvider -class UpdatesGridGlanceWidget : GlanceAppWidget() { - - private val app: Application by injectLazy() - private val preferences: SecurityPreferences by injectLazy() - - private var data: List>? = null - - override val sizeMode = SizeMode.Exact - - override suspend fun provideGlance(context: Context, id: GlanceId) { - val locked = preferences.useAuthenticator().get() - if (!locked) loadData() - - provideContent { - // If app lock enabled, don't do anything - if (locked) { - LockedWidget() - return@provideContent - } - UpdatesWidget(data) - } - } - - private suspend fun loadData(list: List? = null) { - withIOContext { - val manager = GlanceAppWidgetManager(app) - val ids = manager.getGlanceIds(this@UpdatesGridGlanceWidget::class.java) - if (ids.isEmpty()) return@withIOContext - - val processList = list - ?: Injekt.get().await( - read = false, - after = DateLimit.timeInMillis, - ) - val (rowCount, columnCount) = ids - .flatMap { manager.getAppWidgetSizes(it) } - .maxBy { it.height.value * it.width.value } - .calculateRowAndColumnCount() - - data = prepareList(processList, rowCount * columnCount) - } - } - - private fun prepareList(processList: List, take: Int): List> { - // Resize to cover size - val widthPx = CoverWidth.value.toInt().dpToPx - val heightPx = CoverHeight.value.toInt().dpToPx - val roundPx = app.resources.getDimension(R.dimen.appwidget_inner_radius) - return processList - .distinctBy { it.mangaId } - .take(take) - .map { updatesView -> - val request = ImageRequest.Builder(app) - .data( - MangaCover( - mangaId = updatesView.mangaId, - sourceId = updatesView.sourceId, - isMangaFavorite = true, - url = updatesView.coverData.url, - lastModified = updatesView.coverData.lastModified, - ), - ) - .memoryCachePolicy(CachePolicy.DISABLED) - .precision(Precision.EXACT) - .size(widthPx, heightPx) - .scale(Scale.FILL) - .let { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - it.transformations(RoundedCornersTransformation(roundPx)) - } else { - it // Handled by system - } - } - .build() - Pair(updatesView.mangaId, app.imageLoader.executeBlocking(request).drawable?.toBitmap()) - } - } - - companion object { - val DateLimit: Calendar - get() = Calendar.getInstance().apply { - time = Date() - add(Calendar.MONTH, -3) - } - } +class UpdatesGridGlanceWidget : BaseUpdatesGridGlanceWidget() { + override val foreground = ColorProvider(R.color.appwidget_on_secondary_container) + override val background = ImageProvider(R.drawable.appwidget_background) + override val topPadding = 0.dp + override val bottomPadding = 0.dp } - -val ContainerModifier = GlanceModifier - .fillMaxSize() - .background(ImageProvider(R.drawable.appwidget_background)) - .appWidgetBackground() - .appWidgetBackgroundRadius() diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt index 730ab0670..6acb8d7ec 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt @@ -16,26 +16,27 @@ import androidx.glance.text.TextAlign import androidx.glance.text.TextStyle import androidx.glance.unit.ColorProvider import tachiyomi.core.Constants -import tachiyomi.presentation.widget.ContainerModifier import tachiyomi.presentation.widget.R import tachiyomi.presentation.widget.util.stringResource @Composable -fun LockedWidget() { +fun LockedWidget( + foreground: ColorProvider, + modifier: GlanceModifier = GlanceModifier, +) { val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } Box( - modifier = GlanceModifier + modifier = modifier .clickable(actionStartActivity(intent)) - .then(ContainerModifier) .padding(8.dp), contentAlignment = Alignment.Center, ) { Text( text = stringResource(R.string.appwidget_unavailable_locked), style = TextStyle( - color = ColorProvider(R.color.appwidget_on_secondary_container), + color = foreground, fontSize = 12.sp, textAlign = TextAlign.Center, ), diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt index 888acec88..5b537f45d 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt @@ -17,10 +17,7 @@ val CoverWidth = 58.dp val CoverHeight = 87.dp @Composable -fun UpdatesMangaCover( - modifier: GlanceModifier = GlanceModifier, - cover: Bitmap?, -) { +fun UpdatesMangaCover(modifier: GlanceModifier = GlanceModifier, cover: Bitmap?) { Box( modifier = modifier .size(width = CoverWidth, height = CoverHeight) diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt index f7ecd2e71..63a3b329d 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt @@ -3,6 +3,7 @@ package tachiyomi.presentation.widget.components import android.content.Intent import android.graphics.Bitmap import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.glance.GlanceModifier import androidx.glance.LocalContext @@ -14,59 +15,75 @@ import androidx.glance.layout.Alignment import androidx.glance.layout.Box import androidx.glance.layout.Column import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.padding import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider import tachiyomi.core.Constants -import tachiyomi.presentation.widget.ContainerModifier import tachiyomi.presentation.widget.R import tachiyomi.presentation.widget.util.calculateRowAndColumnCount import tachiyomi.presentation.widget.util.stringResource @Composable -fun UpdatesWidget(data: List>?) { - val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount() - Column( - modifier = ContainerModifier, - verticalAlignment = Alignment.CenterVertically, - horizontalAlignment = Alignment.CenterHorizontally, +fun UpdatesWidget( + data: List>?, + modifier: GlanceModifier = GlanceModifier, + contentColor: ColorProvider, + topPadding: Dp, + bottomPadding: Dp, +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier, ) { if (data == null) { - CircularProgressIndicator() + CircularProgressIndicator(color = contentColor) } else if (data.isEmpty()) { - Text(text = stringResource(R.string.information_no_recent)) + Text( + text = stringResource(R.string.information_no_recent), + style = TextStyle(color = contentColor), + ) } else { - (0.. - val coverRow = (0.. - data.getOrNull(j + (i * columnCount)) - } - if (coverRow.isNotEmpty()) { - Row( - modifier = GlanceModifier - .padding(vertical = 4.dp) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalAlignment = Alignment.CenterVertically, - ) { - coverRow.forEach { (mangaId, cover) -> - Box( - modifier = GlanceModifier - .padding(horizontal = 3.dp), - contentAlignment = Alignment.Center, - ) { - val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { - action = Constants.SHORTCUT_MANGA - putExtra(Constants.MANGA_EXTRA, mangaId) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount(topPadding, bottomPadding) + Column( + modifier = GlanceModifier.fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + (0.. + val coverRow = (0.. + data.getOrNull(j + (i * columnCount)) + } + if (coverRow.isNotEmpty()) { + Row( + modifier = GlanceModifier + .padding(vertical = 4.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) { + coverRow.forEach { (mangaId, cover) -> + Box( + modifier = GlanceModifier + .padding(horizontal = 3.dp), + contentAlignment = Alignment.Center, + ) { + val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { + action = Constants.SHORTCUT_MANGA + putExtra(Constants.MANGA_EXTRA, mangaId) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - // https://issuetracker.google.com/issues/238793260 - addCategory(mangaId.toString()) + // https://issuetracker.google.com/issues/238793260 + addCategory(mangaId.toString()) + } + UpdatesMangaCover( + modifier = GlanceModifier.clickable(actionStartActivity(intent)), + cover = cover, + ) } - UpdatesMangaCover( - modifier = GlanceModifier.clickable(actionStartActivity(intent)), - cover = cover, - ) } } } diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt index fe8360a0e..51710a1be 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt @@ -2,6 +2,7 @@ package tachiyomi.presentation.widget.util import androidx.annotation.StringRes import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.glance.GlanceModifier import androidx.glance.LocalContext @@ -34,9 +35,13 @@ fun stringResource(@StringRes id: Int): String { * * @return pair of row and column count */ -fun DpSize.calculateRowAndColumnCount(): Pair { +fun DpSize.calculateRowAndColumnCount( + topPadding: Dp, + bottomPadding: Dp, +): Pair { // Hack: Size provided by Glance manager is not reliable so take at least 1 row and 1 column // Set max to 10 children each direction because of Glance limitation + val height = this.height - topPadding - bottomPadding val rowCount = (height.value / 95).toInt().coerceIn(1, 10) val columnCount = (width.value / 64).toInt().coerceIn(1, 10) return Pair(rowCount, columnCount) diff --git a/presentation-widget/src/main/res/drawable-nodpi/updates_grid_coverscreen_widget_preview.webp b/presentation-widget/src/main/res/drawable-nodpi/updates_grid_coverscreen_widget_preview.webp new file mode 100644 index 000000000..b28fb91b9 Binary files /dev/null and b/presentation-widget/src/main/res/drawable-nodpi/updates_grid_coverscreen_widget_preview.webp differ diff --git a/app/src/main/res/drawable-nodpi/updates_grid_widget_preview.webp b/presentation-widget/src/main/res/drawable-nodpi/updates_grid_widget_preview.webp similarity index 100% rename from app/src/main/res/drawable-nodpi/updates_grid_widget_preview.webp rename to presentation-widget/src/main/res/drawable-nodpi/updates_grid_widget_preview.webp diff --git a/presentation-widget/src/main/res/drawable/appwidget_coverscreen_background.xml b/presentation-widget/src/main/res/drawable/appwidget_coverscreen_background.xml new file mode 100644 index 000000000..066a95b17 --- /dev/null +++ b/presentation-widget/src/main/res/drawable/appwidget_coverscreen_background.xml @@ -0,0 +1,5 @@ + + + + diff --git a/presentation-widget/src/main/res/layout/appwidget_coverscreen_loading.xml b/presentation-widget/src/main/res/layout/appwidget_coverscreen_loading.xml new file mode 100644 index 000000000..057df58f8 --- /dev/null +++ b/presentation-widget/src/main/res/layout/appwidget_coverscreen_loading.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/presentation-widget/src/main/res/values/colors_appwidget.xml b/presentation-widget/src/main/res/values/colors_appwidget.xml index 7d07ea1f8..a5526c94c 100644 --- a/presentation-widget/src/main/res/values/colors_appwidget.xml +++ b/presentation-widget/src/main/res/values/colors_appwidget.xml @@ -1,6 +1,7 @@ @color/tachiyomi_surface + #00000000 @color/tachiyomi_onSurface @color/tachiyomi_surfaceVariant @color/tachiyomi_onSurfaceVariant diff --git a/app/src/main/res/xml/updates_grid_glance_widget_info.xml b/presentation-widget/src/main/res/xml/updates_grid_homescreen_widget_info.xml similarity index 100% rename from app/src/main/res/xml/updates_grid_glance_widget_info.xml rename to presentation-widget/src/main/res/xml/updates_grid_homescreen_widget_info.xml diff --git a/presentation-widget/src/main/res/xml/updates_grid_lockscreen_widget_info.xml b/presentation-widget/src/main/res/xml/updates_grid_lockscreen_widget_info.xml new file mode 100644 index 000000000..62a91467c --- /dev/null +++ b/presentation-widget/src/main/res/xml/updates_grid_lockscreen_widget_info.xml @@ -0,0 +1,7 @@ + + diff --git a/presentation-widget/src/main/res/xml/updates_grid_samsung_cover_widget_info.xml b/presentation-widget/src/main/res/xml/updates_grid_samsung_cover_widget_info.xml new file mode 100644 index 000000000..5f20ff228 --- /dev/null +++ b/presentation-widget/src/main/res/xml/updates_grid_samsung_cover_widget_info.xml @@ -0,0 +1,4 @@ + + diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index f9e416def..937fa28a9 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import rx.Observable +import tachiyomi.core.util.lang.awaitSingle interface CatalogueSource : Source { @@ -17,30 +18,63 @@ interface CatalogueSource : Source { val supportsLatest: Boolean /** - * Returns an observable containing a page with a list of manga. + * Get a page with a list of manga. * + * @since extensions-lib 1.5 * @param page the page number to retrieve. */ - fun fetchPopularManga(page: Int): Observable + @Suppress("DEPRECATION") + suspend fun getPopularManga(page: Int): MangasPage { + return fetchPopularManga(page).awaitSingle() + } /** - * Returns an observable containing a page with a list of manga. + * Get a page with a list of manga. * + * @since extensions-lib 1.5 * @param page the page number to retrieve. * @param query the search query. * @param filters the list of filters to apply. */ - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable + @Suppress("DEPRECATION") + suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { + return fetchSearchManga(page, query, filters).awaitSingle() + } /** - * Returns an observable containing a page with a list of latest manga updates. + * Get a page with a list of latest manga updates. * + * @since extensions-lib 1.5 * @param page the page number to retrieve. */ - fun fetchLatestUpdates(page: Int): Observable + @Suppress("DEPRECATION") + suspend fun getLatestUpdates(page: Int): MangasPage { + return fetchLatestUpdates(page).awaitSingle() + } /** * Returns the list of filters for the source. */ fun getFilterList(): FilterList + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getPopularManga"), + ) + fun fetchPopularManga(page: Int): Observable = + throw IllegalStateException("Not used") + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getSearchManga"), + ) + fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = + throw IllegalStateException("Not used") + + @Deprecated( + "Use the non-RxJava API instead", + ReplaceWith("getLatestUpdates"), + ) + fun fetchLatestUpdates(page: Int): Observable = + throw IllegalStateException("Not used") } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt index d42885f80..db9a98520 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt @@ -1,6 +1,29 @@ package eu.kanade.tachiyomi.source +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + interface ConfigurableSource : Source { + /** + * Gets instance of [SharedPreferences] scoped to the specific source. + * + * @since extensions-lib 1.5 + */ + fun getSourcePreferences(): SharedPreferences = + Injekt.get().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE) + fun setupPreferenceScreen(screen: PreferenceScreen) } + +fun ConfigurableSource.preferenceKey(): String = "source_$id" + +// TODO: use getSourcePreferences once all extensions are on ext-lib 1.5 +fun ConfigurableSource.sourcePreferences(): SharedPreferences = + Injekt.get().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE) + +fun sourcePreferences(key: String): SharedPreferences = + Injekt.get().getSharedPreferences(key, Context.MODE_PRIVATE) diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index 15747af98..83fcd7962 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -27,7 +27,9 @@ interface Source { /** * Get the updated details for a manga. * + * @since extensions-lib 1.5 * @param manga the manga to update. + * @return the updated manga. */ @Suppress("DEPRECATION") suspend fun getMangaDetails(manga: SManga): SManga { @@ -37,7 +39,9 @@ interface Source { /** * Get all the available chapters for a manga. * + * @since extensions-lib 1.5 * @param manga the manga to update. + * @return the chapters for the manga. */ @Suppress("DEPRECATION") suspend fun getChapterList(manga: SManga): List { @@ -48,44 +52,33 @@ interface Source { * Get the list of pages a chapter has. Pages should be returned * in the expected order; the index is ignored. * + * @since extensions-lib 1.5 * @param chapter the chapter. + * @return the pages for the chapter. */ @Suppress("DEPRECATION") suspend fun getPageList(chapter: SChapter): List { return fetchPageList(chapter).awaitSingle() } - /** - * Returns an observable with the updated details for a manga. - * - * @param manga the manga to update. - */ @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getMangaDetails"), ) - fun fetchMangaDetails(manga: SManga): Observable = throw IllegalStateException("Not used") + fun fetchMangaDetails(manga: SManga): Observable = + throw IllegalStateException("Not used") - /** - * Returns an observable with all the available chapters for a manga. - * - * @param manga the manga to update. - */ @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getChapterList"), ) - fun fetchChapterList(manga: SManga): Observable> = throw IllegalStateException("Not used") + fun fetchChapterList(manga: SManga): Observable> = + throw IllegalStateException("Not used") - /** - * Returns an observable with the list of pages a chapter has. Pages should be returned - * in the expected order; the index is ignored. - * - * @param chapter the chapter. - */ @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getPageList"), ) - fun fetchPageList(chapter: SChapter): Observable> = Observable.empty() + fun fetchPageList(chapter: SChapter): Observable> = + throw IllegalStateException("Not used") } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt index f30b2f52d..99c72e58e 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt @@ -3,7 +3,10 @@ package eu.kanade.tachiyomi.source.model sealed class Filter(val name: String, var state: T) { open class Header(name: String) : Filter(name, 0) open class Separator(name: String = "") : Filter(name, 0) - abstract class Select(name: String, val values: Array, state: Int = 0) : Filter(name, state) + abstract class Select(name: String, val values: Array, state: Int = 0) : Filter( + name, + state, + ) abstract class Text(name: String, state: String = "") : Filter(name, state) abstract class CheckBox(name: String, state: Boolean = false) : Filter(name, state) abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter(name, state) { diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 7378f43a8..957febceb 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -16,6 +16,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable +import tachiyomi.core.util.lang.awaitSingle import uy.kohesive.injekt.injectLazy import java.net.URI import java.net.URISyntaxException @@ -24,6 +25,7 @@ import java.security.MessageDigest /** * A simple implementation for sources from a website. */ +@Suppress("unused") abstract class HttpSource : CatalogueSource { /** @@ -81,6 +83,7 @@ abstract class HttpSource : CatalogueSource { * @param versionId [Int] the version ID of the source * @return a unique ID for the source */ + @Suppress("MemberVisibilityCanBePrivate") protected fun generateId(name: String, lang: String, versionId: Int): Long { val key = "${name.lowercase()}/$lang/$versionId" val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) @@ -135,7 +138,11 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable { return Observable.defer { try { client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess() @@ -157,7 +164,11 @@ abstract class HttpSource : CatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ - protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request + protected abstract fun searchMangaRequest( + page: Int, + query: String, + filters: FilterList, + ): Request /** * Parses the response from the site and returns a [MangasPage] object. @@ -194,11 +205,18 @@ abstract class HttpSource : CatalogueSource { protected abstract fun latestUpdatesParse(response: Response): MangasPage /** - * Returns an observable with the updated details for a manga. Normally it's not needed to - * override this method. + * Get the updated details for a manga. + * Normally it's not needed to override this method. * - * @param manga the manga to be updated. + * @param manga the manga to update. + * @return the updated manga. */ + @Suppress("DEPRECATION") + override suspend fun getMangaDetails(manga: SManga): SManga { + return fetchMangaDetails(manga).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails")) override fun fetchMangaDetails(manga: SManga): Observable { return client.newCall(mangaDetailsRequest(manga)) .asObservableSuccess() @@ -225,11 +243,23 @@ abstract class HttpSource : CatalogueSource { protected abstract fun mangaDetailsParse(response: Response): SManga /** - * Returns an observable with the updated chapter list for a manga. Normally it's not needed to - * override this method. If a manga is licensed an empty chapter list observable is returned + * Get all the available chapters for a manga. + * Normally it's not needed to override this method. * - * @param manga the manga to look for chapters. + * @param manga the manga to update. + * @return the chapters for the manga. + * @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available. */ + @Suppress("DEPRECATION") + override suspend fun getChapterList(manga: SManga): List { + if (manga.status == SManga.LICENSED) { + throw LicensedMangaChaptersException() + } + + return fetchChapterList(manga).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList")) override fun fetchChapterList(manga: SManga): Observable> { return if (manga.status != SManga.LICENSED) { client.newCall(chapterListRequest(manga)) @@ -260,10 +290,18 @@ abstract class HttpSource : CatalogueSource { protected abstract fun chapterListParse(response: Response): List /** - * Returns an observable with the page list for a chapter. + * Get the list of pages a chapter has. Pages should be returned + * in the expected order; the index is ignored. * - * @param chapter the chapter whose page list has to be fetched. + * @param chapter the chapter. + * @return the pages for the chapter. */ + @Suppress("DEPRECATION") + override suspend fun getPageList(chapter: SChapter): List { + return fetchPageList(chapter).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList")) override fun fetchPageList(chapter: SChapter): Observable> { return client.newCall(pageListRequest(chapter)) .asObservableSuccess() @@ -293,8 +331,15 @@ abstract class HttpSource : CatalogueSource { * Returns an observable with the page containing the source url of the image. If there's any * error, it will return null instead of throwing an exception. * + * @since extensions-lib 1.5 * @param page the page whose source image has to be fetched. */ + @Suppress("DEPRECATION") + open suspend fun getImageUrl(page: Page): String { + return fetchImageUrl(page).awaitSingle() + } + + @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) open fun fetchImageUrl(page: Page): Observable { return client.newCall(imageUrlRequest(page)) .asObservableSuccess() @@ -318,24 +363,14 @@ abstract class HttpSource : CatalogueSource { */ protected abstract fun imageUrlParse(response: Response): String - /** - * Returns an observable with the response of the source image. - * - * @param page the page whose source image has to be downloaded. - */ - fun fetchImage(page: Page): Observable { - // images will be cached or saved manually, so don't take up network cache - return client.newCachelessCallWithProgress(imageRequest(page), page) - .asObservableSuccess() - } - /** * Returns the response of the source image. + * Typically does not need to be overridden. * + * @since extensions-lib 1.5 * @param page the page whose source image has to be downloaded. */ - suspend fun getImage(page: Page): Response { - // images will be cached or saved manually, so don't take up network cache + open suspend fun getImage(page: Page): Response { return client.newCachelessCallWithProgress(imageRequest(page), page) .awaitSuccess() } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt deleted file mode 100644 index 76c68e882..000000000 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSourceFetcher.kt +++ /dev/null @@ -1,25 +0,0 @@ -package eu.kanade.tachiyomi.source.online - -import eu.kanade.tachiyomi.source.model.Page -import rx.Observable - -fun HttpSource.fetchAllImageUrlsFromPageList(pages: List): Observable { - return Observable.from(pages) - .filter { !it.imageUrl.isNullOrEmpty() } - .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) -} - -private fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List): Observable { - return Observable.from(pages) - .filter { it.imageUrl.isNullOrEmpty() } - .concatMap { getImageUrl(it) } -} - -private fun HttpSource.getImageUrl(page: Page): Observable { - page.status = Page.State.LOAD_PAGE - return fetchImageUrl(page) - .doOnError { page.status = Page.State.ERROR } - .onErrorReturn { null } - .doOnNext { page.imageUrl = it } - .map { page } -} diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 403419df2..636d205f2 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -1,6 +1,7 @@ package tachiyomi.source.local import android.content.Context +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.UnmeteredSource @@ -10,16 +11,15 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.EpubFile -import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML -import rx.Observable import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo +import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.system.ImageUtil @@ -66,11 +66,11 @@ actual class LocalSource( override val supportsLatest: Boolean = true // Browse related - override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS) + override suspend fun getPopularManga(page: Int) = getSearchManga(page, "", POPULAR_FILTERS) - override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS) + override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS) - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { val baseDirsFiles = fileSystem.getFilesInBaseDirectories() val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L } var mangaDirs = baseDirsFiles @@ -123,27 +123,25 @@ actual class LocalSource( // Fetch chapters of all the manga mangas.forEach { manga -> - runBlocking { - val chapters = getChapterList(manga) - if (chapters.isNotEmpty()) { - val chapter = chapters.last() - val format = getFormat(chapter) + val chapters = getChapterList(manga) + if (chapters.isNotEmpty()) { + val chapter = chapters.last() + val format = getFormat(chapter) - if (format is Format.Epub) { - EpubFile(format.file).use { epub -> - epub.fillMangaMetadata(manga) - } + if (format is Format.Epub) { + EpubFile(format.file).use { epub -> + epub.fillMangaMetadata(manga) } + } - // Copy the cover from the first chapter found if not available - if (manga.thumbnail_url == null) { - updateCover(chapter, manga) - } + // Copy the cover from the first chapter found if not available + if (manga.thumbnail_url == null) { + updateCover(chapter, manga) } } } - return Observable.just(MangasPage(mangas.toList(), false)) + return MangasPage(mangas.toList(), false) } // Manga details related @@ -154,6 +152,7 @@ actual class LocalSource( // Augment manga details based on metadata files try { + val mangaDir = fileSystem.getMangaDirectory(manga.url) val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList() val comicInfoFile = mangaDirFiles @@ -170,7 +169,8 @@ actual class LocalSource( setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga) } - // TODO: automatically convert these to ComicInfo.xml + // Old custom JSON format + // TODO: remove support for this entirely after a while legacyJsonDetailsFile != null -> { json.decodeFromStream(legacyJsonDetailsFile.inputStream()).run { title?.let { manga.title = it } @@ -180,6 +180,16 @@ actual class LocalSource( genre?.let { manga.genre = it.joinToString() } status?.let { manga.status = it } } + // Replace with ComicInfo.xml file + val comicInfo = manga.getComicInfo() + UniFile.fromFile(mangaDir) + ?.createFile(COMIC_INFO_FILE) + ?.openOutputStream() + ?.use { + val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo) + it.write(comicInfoString.toByteArray()) + legacyJsonDetailsFile.delete() + } } // Copy ComicInfo.xml from chapter archive to top level if found @@ -188,7 +198,6 @@ actual class LocalSource( .filter(Archive::isSupported) .toList() - val mangaDir = fileSystem.getMangaDirectory(manga.url) val folderPath = mangaDir?.absolutePath val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath) @@ -349,7 +358,7 @@ actual class LocalSource( companion object { const val ID = 0L - const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/" + const val HELP_URL = "https://tachiyomi.org/docs/guides/local-source/" private val LATEST_THRESHOLD = 7.days.inWholeMilliseconds } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index e5aaf5dd7..7683756e3 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -18,7 +18,7 @@ actual class LocalCoverManager( actual fun find(mangaUrl: String): File? { return fileSystem.getFilesInMangaDirectory(mangaUrl) - // Get all file whose names start with 'cover' + // Get all file whose names start with "cover" .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) } // Get the first actual image .firstOrNull {