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>
This commit is contained in:
David Brochero 2026-02-02 06:47:34 -03:00 committed by GitHub
parent c7e0585c6f
commit e7bdbb5794
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 78 additions and 1 deletions

View file

@ -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 <--
),
)

View file

@ -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<BulkFavoriteScreenModel.State>(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}")
}
}
}
}
}

View file

@ -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 <--
}
}

View file

@ -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,

View file

@ -96,6 +96,8 @@
<string name="pref_library_filter_categories_details">Entries in excluded categories will not be shown even if they are also in included categories.</string>
<string name="updating">Updating</string>
<string name="pref_show_empty_categories_search">Show empty categories during search/filtering</string>
<string name="pref_sync_manga_on_add">Sync manga when adding to library</string>
<string name="pref_sync_manga_on_add_description">Automatically fetches the metadata and chapter list of a new manga when adding it to the library</string>
<!-- Extension section -->
<string name="extensions_page_need_refresh">Refresh extension page to update list.</string>
<string name="extensions_page_more">More extensions...</string>