From e7bdbb5794eccbecd57b23bf5f62c0cc6747604d Mon Sep 17 00:00:00 2001 From: David Brochero <65723952+D-Brox@users.noreply.github.com> Date: Mon, 2 Feb 2026 06:47:34 -0300 Subject: [PATCH] feat: option to auto sync metadata and chapter list when adding to library (#1091) * feat: auto-fetch chapter list when adding to library * simplify impl * IO context (gemini suggestion) * don't trigger NoChapterException * add option to settings * linting * fix: fetching on bulk * add kmk comment * fix exception handling * linting again * feat: also sync metadata * fix: rebase issues * remove KMK comment * Use `manga` instead of `new` so its title got updated with source's `getMangaDetails`; also reset chapterFlags --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- .../settings/screen/SettingsLibraryScreen.kt | 5 +++ .../ui/browse/BulkFavoriteScreenModel.kt | 41 ++++++++++++++++++- .../source/browse/BrowseSourceScreenModel.kt | 27 ++++++++++++ .../library/service/LibraryPreferences.kt | 4 ++ .../moko-resources/base/strings.xml | 2 + 5 files changed, 78 insertions(+), 1 deletion(-) 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 02362b1a1..ccd66c659 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 @@ -292,6 +292,11 @@ object SettingsLibraryScreen : SearchableSettings { preference = libraryPreferences.showEmptyCategoriesSearch(), title = stringResource(KMR.strings.pref_show_empty_categories_search), ), + Preference.PreferenceItem.SwitchPreference( + preference = libraryPreferences.syncOnAdd(), + title = stringResource(KMR.strings.pref_sync_manga_on_add), + subtitle = stringResource(KMR.strings.pref_sync_manga_on_add_description), + ), // KMK <-- ), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index 0d0d06047..4000dbbeb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.browse +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -9,7 +10,9 @@ import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope +import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.presentation.components.BulkSelectionToolbar import eu.kanade.presentation.manga.DuplicateMangaDialog @@ -24,10 +27,13 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import logcat.LogPriority import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable +import tachiyomi.core.common.util.lang.withIOContext +import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category @@ -53,6 +59,8 @@ class BulkFavoriteScreenModel( private val coverCache: CoverCache = Injekt.get(), private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), private val addTracks: AddTracks = Injekt.get(), + private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), + val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(initialState) { fun backHandler() { @@ -243,7 +251,23 @@ class BulkFavoriteScreenModel( if (manga.favorite) return screenModelScope.launchIO { - updateManga.awaitUpdateFavorite(manga.id, true) + try { + val source = sourceManager.getOrStub(manga.source) + setMangaDefaultChapterFlags.await(manga) + addTracks.bindEnhancedTrackers(manga, source) + updateManga.awaitUpdateFavorite(manga.id, true) + if (libraryPreferences.syncOnAdd().get()) { + val sManga = manga.toSManga() + val remoteManga = source.getMangaDetails(sManga) + val chapters = source.getChapterList(sManga) + // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` + updateManga.awaitUpdateFromSource(manga, remoteManga, false, coverCache) + syncChaptersWithSource.await(chapters, manga, source, false) + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + snackbarHostState.showSnackbar(message = "Failed to sync manga: ${e.message}") + } } } @@ -327,6 +351,21 @@ class BulkFavoriteScreenModel( } updateManga.await(new.toMangaUpdate()) + if (new.favorite && libraryPreferences.syncOnAdd().get()) { + withIOContext { + try { + val sManga = manga.toSManga() + val remoteManga = source.getMangaDetails(sManga) + val chapters = source.getChapterList(sManga) + // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` + updateManga.awaitUpdateFromSource(manga, remoteManga, false, coverCache) + syncChaptersWithSource.await(chapters, manga, source, false) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + snackbarHostState.showSnackbar(message = "Failed to sync manga: ${e.message}") + } + } + } } } 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 9e6c79425..75b8219c9 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 @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.browse import android.content.res.Configuration import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,7 +17,9 @@ import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import dev.icerock.moko.resources.StringResource import eu.kanade.core.preference.asState +import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.source.interactor.GetExhSavedSearch import eu.kanade.domain.source.interactor.GetIncognitoState import eu.kanade.domain.source.interactor.ToggleIncognito @@ -57,11 +60,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray +import logcat.LogPriority import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable +import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withUIContext +import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.model.Category @@ -113,6 +119,8 @@ open class BrowseSourceScreenModel( // KMK --> private val toggleIncognito: ToggleIncognito = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(), + private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), + val snackbarHostState: SnackbarHostState = SnackbarHostState(), // KMK <-- // SY --> @@ -415,6 +423,25 @@ open class BrowseSourceScreenModel( } updateManga.await(new.toMangaUpdate()) + // KMK --> + if (new.favorite && libraryPreferences.syncOnAdd().get()) { + withIOContext { + try { + val sManga = manga.toSManga() + val remoteManga = source.getMangaDetails(sManga) + val chapters = source.getChapterList(sManga) + // Use `manga` instead of `new` so its title got updated with source's `getMangaDetails` + updateManga.awaitUpdateFromSource(manga, remoteManga, false, coverCache) + syncChaptersWithSource.await(chapters, manga, source, false) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + screenModelScope.launch { + snackbarHostState.showSnackbar(message = "Failed to sync manga: ${e.message}") + } + } + } + } + // KMK <-- } } 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 8873986e8..5209d95b9 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -73,6 +73,10 @@ class LibraryPreferences( fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false) + // KMK --> + fun syncOnAdd() = preferenceStore.getBoolean("sync_on_add", false) + // KMK <-- + fun showContinueReadingButton() = preferenceStore.getBoolean( "display_continue_reading_button", false, diff --git a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml index d909414a1..09375740c 100644 --- a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml @@ -96,6 +96,8 @@ Entries in excluded categories will not be shown even if they are also in included categories. Updating Show empty categories during search/filtering + Sync manga when adding to library + Automatically fetches the metadata and chapter list of a new manga when adding it to the library Refresh extension page to update list. More extensions...