mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Update Manga in Expected Period (#5734)
* Add Predict Interval Test * Get mangas next update and interval in library update * Get next update and interval in backup restore * Display and set intervals, nextUpdate in Manga Info * Move logic function to MangeScreen and InfoHeader Update per suggestion --------- Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
		@@ -57,6 +57,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
 | 
			
		||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
 | 
			
		||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
 | 
			
		||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
 | 
			
		||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
 | 
			
		||||
import tachiyomi.domain.manga.repository.MangaRepository
 | 
			
		||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
 | 
			
		||||
import tachiyomi.domain.release.service.ReleaseService
 | 
			
		||||
@@ -100,10 +101,11 @@ class DomainModule : InjektModule {
 | 
			
		||||
        addFactory { GetNextChapters(get(), get(), get()) }
 | 
			
		||||
        addFactory { ResetViewerFlags(get()) }
 | 
			
		||||
        addFactory { SetMangaChapterFlags(get()) }
 | 
			
		||||
        addFactory { SetMangaUpdateInterval(get()) }
 | 
			
		||||
        addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
 | 
			
		||||
        addFactory { SetMangaViewerFlags(get()) }
 | 
			
		||||
        addFactory { NetworkToLocalManga(get()) }
 | 
			
		||||
        addFactory { UpdateManga(get()) }
 | 
			
		||||
        addFactory { UpdateManga(get(), get()) }
 | 
			
		||||
        addFactory { SetMangaCategories(get()) }
 | 
			
		||||
 | 
			
		||||
        addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ import tachiyomi.source.local.isLocal
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.lang.Long.max
 | 
			
		||||
import java.time.ZonedDateTime
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import java.util.TreeSet
 | 
			
		||||
 | 
			
		||||
@@ -48,6 +49,9 @@ class SyncChaptersWithSource(
 | 
			
		||||
        rawSourceChapters: List<SChapter>,
 | 
			
		||||
        manga: Manga,
 | 
			
		||||
        source: Source,
 | 
			
		||||
        manualFetch: Boolean = false,
 | 
			
		||||
        zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
 | 
			
		||||
        fetchRange: Pair<Long, Long> = Pair(0, 0),
 | 
			
		||||
    ): List<Chapter> {
 | 
			
		||||
        if (rawSourceChapters.isEmpty() && !source.isLocal()) {
 | 
			
		||||
            throw NoChaptersException()
 | 
			
		||||
@@ -134,6 +138,14 @@ class SyncChaptersWithSource(
 | 
			
		||||
 | 
			
		||||
        // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
 | 
			
		||||
        if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
 | 
			
		||||
            if (manualFetch || manga.calculateInterval == 0 || manga.nextUpdate < fetchRange.first) {
 | 
			
		||||
                updateManga.awaitUpdateFetchInterval(
 | 
			
		||||
                    manga,
 | 
			
		||||
                    dbChapters,
 | 
			
		||||
                    zoneDateTime,
 | 
			
		||||
                    fetchRange,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            return emptyList()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -188,6 +200,8 @@ class SyncChaptersWithSource(
 | 
			
		||||
            val chapterUpdates = toChange.map { it.toChapterUpdate() }
 | 
			
		||||
            updateChapter.awaitAll(chapterUpdates)
 | 
			
		||||
        }
 | 
			
		||||
        val newChapters = chapterRepository.getChapterByMangaId(manga.id)
 | 
			
		||||
        updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
 | 
			
		||||
 | 
			
		||||
        // Set this manga as updated since chapters were changed
 | 
			
		||||
        // Note that last_update actually represents last time the chapter list changed at all
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,7 @@ import eu.kanade.domain.manga.model.hasCustomCover
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.CoverCache
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import tachiyomi.domain.chapter.model.Chapter
 | 
			
		||||
import tachiyomi.domain.manga.interactor.getCurrentFetchRange
 | 
			
		||||
import tachiyomi.domain.manga.interactor.updateIntervalMeta
 | 
			
		||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.domain.manga.model.MangaUpdate
 | 
			
		||||
import tachiyomi.domain.manga.repository.MangaRepository
 | 
			
		||||
@@ -17,6 +16,7 @@ import java.util.Date
 | 
			
		||||
 | 
			
		||||
class UpdateManga(
 | 
			
		||||
    private val mangaRepository: MangaRepository,
 | 
			
		||||
    private val setMangaUpdateInterval: SetMangaUpdateInterval,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    suspend fun await(mangaUpdate: MangaUpdate): Boolean {
 | 
			
		||||
@@ -77,16 +77,15 @@ class UpdateManga(
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun awaitUpdateIntervalMeta(
 | 
			
		||||
    suspend fun awaitUpdateFetchInterval(
 | 
			
		||||
        manga: Manga,
 | 
			
		||||
        chapters: List<Chapter>,
 | 
			
		||||
        zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
 | 
			
		||||
        setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
 | 
			
		||||
        fetchRange: Pair<Long, Long> = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime),
 | 
			
		||||
    ): Boolean {
 | 
			
		||||
        val newMeta = updateIntervalMeta(manga, chapters, zonedDateTime, setCurrentFetchRange)
 | 
			
		||||
 | 
			
		||||
        return if (newMeta != null) {
 | 
			
		||||
            mangaRepository.update(newMeta)
 | 
			
		||||
        val updatedManga = setMangaUpdateInterval.updateInterval(manga, chapters, zonedDateTime, fetchRange)
 | 
			
		||||
        return if (updatedManga != null) {
 | 
			
		||||
            mangaRepository.update(updatedManga)
 | 
			
		||||
        } else {
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -85,6 +85,7 @@ fun MangaScreen(
 | 
			
		||||
    state: MangaScreenModel.State.Success,
 | 
			
		||||
    snackbarHostState: SnackbarHostState,
 | 
			
		||||
    dateRelativeTime: Int,
 | 
			
		||||
    intervalDisplay: () -> Pair<Int, Int>?,
 | 
			
		||||
    dateFormat: DateFormat,
 | 
			
		||||
    isTabletUi: Boolean,
 | 
			
		||||
    chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
 | 
			
		||||
@@ -112,6 +113,7 @@ fun MangaScreen(
 | 
			
		||||
    onShareClicked: (() -> Unit)?,
 | 
			
		||||
    onDownloadActionClicked: ((DownloadAction) -> Unit)?,
 | 
			
		||||
    onEditCategoryClicked: (() -> Unit)?,
 | 
			
		||||
    onEditIntervalClicked: (() -> Unit)?,
 | 
			
		||||
    onMigrateClicked: (() -> Unit)?,
 | 
			
		||||
 | 
			
		||||
    // For bottom action menu
 | 
			
		||||
@@ -141,6 +143,7 @@ fun MangaScreen(
 | 
			
		||||
            snackbarHostState = snackbarHostState,
 | 
			
		||||
            dateRelativeTime = dateRelativeTime,
 | 
			
		||||
            dateFormat = dateFormat,
 | 
			
		||||
            intervalDisplay = intervalDisplay,
 | 
			
		||||
            chapterSwipeStartAction = chapterSwipeStartAction,
 | 
			
		||||
            chapterSwipeEndAction = chapterSwipeEndAction,
 | 
			
		||||
            onBackClicked = onBackClicked,
 | 
			
		||||
@@ -160,6 +163,7 @@ fun MangaScreen(
 | 
			
		||||
            onShareClicked = onShareClicked,
 | 
			
		||||
            onDownloadActionClicked = onDownloadActionClicked,
 | 
			
		||||
            onEditCategoryClicked = onEditCategoryClicked,
 | 
			
		||||
            onEditIntervalClicked = onEditIntervalClicked,
 | 
			
		||||
            onMigrateClicked = onMigrateClicked,
 | 
			
		||||
            onMultiBookmarkClicked = onMultiBookmarkClicked,
 | 
			
		||||
            onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
 | 
			
		||||
@@ -178,6 +182,7 @@ fun MangaScreen(
 | 
			
		||||
            chapterSwipeStartAction = chapterSwipeStartAction,
 | 
			
		||||
            chapterSwipeEndAction = chapterSwipeEndAction,
 | 
			
		||||
            dateFormat = dateFormat,
 | 
			
		||||
            intervalDisplay = intervalDisplay,
 | 
			
		||||
            onBackClicked = onBackClicked,
 | 
			
		||||
            onChapterClicked = onChapterClicked,
 | 
			
		||||
            onDownloadChapter = onDownloadChapter,
 | 
			
		||||
@@ -195,6 +200,7 @@ fun MangaScreen(
 | 
			
		||||
            onShareClicked = onShareClicked,
 | 
			
		||||
            onDownloadActionClicked = onDownloadActionClicked,
 | 
			
		||||
            onEditCategoryClicked = onEditCategoryClicked,
 | 
			
		||||
            onEditIntervalClicked = onEditIntervalClicked,
 | 
			
		||||
            onMigrateClicked = onMigrateClicked,
 | 
			
		||||
            onMultiBookmarkClicked = onMultiBookmarkClicked,
 | 
			
		||||
            onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
 | 
			
		||||
@@ -214,6 +220,7 @@ private fun MangaScreenSmallImpl(
 | 
			
		||||
    snackbarHostState: SnackbarHostState,
 | 
			
		||||
    dateRelativeTime: Int,
 | 
			
		||||
    dateFormat: DateFormat,
 | 
			
		||||
    intervalDisplay: () -> Pair<Int, Int>?,
 | 
			
		||||
    chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
 | 
			
		||||
    chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
 | 
			
		||||
    onBackClicked: () -> Unit,
 | 
			
		||||
@@ -240,6 +247,7 @@ private fun MangaScreenSmallImpl(
 | 
			
		||||
    onShareClicked: (() -> Unit)?,
 | 
			
		||||
    onDownloadActionClicked: ((DownloadAction) -> Unit)?,
 | 
			
		||||
    onEditCategoryClicked: (() -> Unit)?,
 | 
			
		||||
    onEditIntervalClicked: (() -> Unit)?,
 | 
			
		||||
    onMigrateClicked: (() -> Unit)?,
 | 
			
		||||
 | 
			
		||||
    // For bottom action menu
 | 
			
		||||
@@ -383,10 +391,13 @@ private fun MangaScreenSmallImpl(
 | 
			
		||||
                        MangaActionRow(
 | 
			
		||||
                            favorite = state.manga.favorite,
 | 
			
		||||
                            trackingCount = state.trackingCount,
 | 
			
		||||
                            intervalDisplay = intervalDisplay,
 | 
			
		||||
                            isUserIntervalMode = state.manga.calculateInterval < 0,
 | 
			
		||||
                            onAddToLibraryClicked = onAddToLibraryClicked,
 | 
			
		||||
                            onWebViewClicked = onWebViewClicked,
 | 
			
		||||
                            onWebViewLongClicked = onWebViewLongClicked,
 | 
			
		||||
                            onTrackingClicked = onTrackingClicked,
 | 
			
		||||
                            onEditIntervalClicked = onEditIntervalClicked,
 | 
			
		||||
                            onEditCategory = onEditCategoryClicked,
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
@@ -440,6 +451,7 @@ fun MangaScreenLargeImpl(
 | 
			
		||||
    snackbarHostState: SnackbarHostState,
 | 
			
		||||
    dateRelativeTime: Int,
 | 
			
		||||
    dateFormat: DateFormat,
 | 
			
		||||
    intervalDisplay: () -> Pair<Int, Int>?,
 | 
			
		||||
    chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
 | 
			
		||||
    chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
 | 
			
		||||
    onBackClicked: () -> Unit,
 | 
			
		||||
@@ -466,6 +478,7 @@ fun MangaScreenLargeImpl(
 | 
			
		||||
    onShareClicked: (() -> Unit)?,
 | 
			
		||||
    onDownloadActionClicked: ((DownloadAction) -> Unit)?,
 | 
			
		||||
    onEditCategoryClicked: (() -> Unit)?,
 | 
			
		||||
    onEditIntervalClicked: (() -> Unit)?,
 | 
			
		||||
    onMigrateClicked: (() -> Unit)?,
 | 
			
		||||
 | 
			
		||||
    // For bottom action menu
 | 
			
		||||
@@ -596,10 +609,13 @@ fun MangaScreenLargeImpl(
 | 
			
		||||
                        MangaActionRow(
 | 
			
		||||
                            favorite = state.manga.favorite,
 | 
			
		||||
                            trackingCount = state.trackingCount,
 | 
			
		||||
                            intervalDisplay = intervalDisplay,
 | 
			
		||||
                            isUserIntervalMode = state.manga.calculateInterval < 0,
 | 
			
		||||
                            onAddToLibraryClicked = onAddToLibraryClicked,
 | 
			
		||||
                            onWebViewClicked = onWebViewClicked,
 | 
			
		||||
                            onWebViewLongClicked = onWebViewLongClicked,
 | 
			
		||||
                            onTrackingClicked = onTrackingClicked,
 | 
			
		||||
                            onEditIntervalClicked = onEditIntervalClicked,
 | 
			
		||||
                            onEditCategory = onEditCategoryClicked,
 | 
			
		||||
                        )
 | 
			
		||||
                        ExpandableMangaDescription(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,23 @@
 | 
			
		||||
package eu.kanade.presentation.manga.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.BoxWithConstraints
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.material3.AlertDialog
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.material3.TextButton
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableIntStateOf
 | 
			
		||||
import androidx.compose.runtime.saveable.rememberSaveable
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
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_GRACE_PERIOD
 | 
			
		||||
import tachiyomi.presentation.core.components.WheelTextPicker
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun DeleteChaptersDialog(
 | 
			
		||||
@@ -37,3 +49,51 @@ fun DeleteChaptersDialog(
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SetIntervalDialog(
 | 
			
		||||
    interval: Int,
 | 
			
		||||
    onDismissRequest: () -> Unit,
 | 
			
		||||
    onValueChanged: (Int) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
 | 
			
		||||
 | 
			
		||||
    AlertDialog(
 | 
			
		||||
        onDismissRequest = onDismissRequest,
 | 
			
		||||
        title = { Text(text = stringResource(R.string.manga_modify_calculated_interval_title)) },
 | 
			
		||||
        text = {
 | 
			
		||||
            BoxWithConstraints(
 | 
			
		||||
                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                contentAlignment = Alignment.Center,
 | 
			
		||||
            ) {
 | 
			
		||||
                val size = DpSize(width = maxWidth / 2, height = 128.dp)
 | 
			
		||||
                val items = (0..MAX_GRACE_PERIOD).map {
 | 
			
		||||
                    if (it == 0) {
 | 
			
		||||
                        stringResource(R.string.label_default)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        it.toString()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                WheelTextPicker(
 | 
			
		||||
                    size = size,
 | 
			
		||||
                    items = items,
 | 
			
		||||
                    startIndex = intervalValue,
 | 
			
		||||
                    onSelectionChanged = { intervalValue = it },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        dismissButton = {
 | 
			
		||||
            TextButton(onClick = onDismissRequest) {
 | 
			
		||||
                Text(text = stringResource(R.string.action_cancel))
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        confirmButton = {
 | 
			
		||||
            TextButton(onClick = {
 | 
			
		||||
                onValueChanged(intervalValue)
 | 
			
		||||
                onDismissRequest()
 | 
			
		||||
            },) {
 | 
			
		||||
                Text(text = stringResource(R.string.action_ok))
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ import androidx.compose.foundation.lazy.items
 | 
			
		||||
import androidx.compose.foundation.text.selection.SelectionContainer
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.Favorite
 | 
			
		||||
import androidx.compose.material.icons.filled.HourglassEmpty
 | 
			
		||||
import androidx.compose.material.icons.filled.Warning
 | 
			
		||||
import androidx.compose.material.icons.outlined.AttachMoney
 | 
			
		||||
import androidx.compose.material.icons.outlined.Block
 | 
			
		||||
@@ -164,14 +165,19 @@ fun MangaActionRow(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    favorite: Boolean,
 | 
			
		||||
    trackingCount: Int,
 | 
			
		||||
    intervalDisplay: () -> Pair<Int, Int>?,
 | 
			
		||||
    isUserIntervalMode: Boolean,
 | 
			
		||||
    onAddToLibraryClicked: () -> Unit,
 | 
			
		||||
    onWebViewClicked: (() -> Unit)?,
 | 
			
		||||
    onWebViewLongClicked: (() -> Unit)?,
 | 
			
		||||
    onTrackingClicked: (() -> Unit)?,
 | 
			
		||||
    onEditIntervalClicked: (() -> Unit)?,
 | 
			
		||||
    onEditCategory: (() -> Unit)?,
 | 
			
		||||
) {
 | 
			
		||||
    val interval: Pair<Int, Int>? = intervalDisplay()
 | 
			
		||||
    val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
 | 
			
		||||
 | 
			
		||||
    Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
 | 
			
		||||
        val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
 | 
			
		||||
        MangaActionButton(
 | 
			
		||||
            title = if (favorite) {
 | 
			
		||||
                stringResource(R.string.in_library)
 | 
			
		||||
@@ -183,6 +189,19 @@ fun MangaActionRow(
 | 
			
		||||
            onClick = onAddToLibraryClicked,
 | 
			
		||||
            onLongClick = onEditCategory,
 | 
			
		||||
        )
 | 
			
		||||
        if (onEditIntervalClicked != null && interval != null) {
 | 
			
		||||
            MangaActionButton(
 | 
			
		||||
                title =
 | 
			
		||||
                if (interval.first == interval.second) {
 | 
			
		||||
                    pluralStringResource(id = R.plurals.day, count = interval.second, interval.second)
 | 
			
		||||
                } else {
 | 
			
		||||
                    pluralStringResource(id = R.plurals.range_interval_day, count = interval.second, interval.first, interval.second)
 | 
			
		||||
                },
 | 
			
		||||
                icon = Icons.Default.HourglassEmpty,
 | 
			
		||||
                color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
 | 
			
		||||
                onClick = onEditIntervalClicked,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        if (onTrackingClicked != null) {
 | 
			
		||||
            MangaActionButton(
 | 
			
		||||
                title = if (trackingCount == 0) {
 | 
			
		||||
 
 | 
			
		||||
@@ -241,18 +241,15 @@ object SettingsLibraryScreen : SearchableSettings {
 | 
			
		||||
                    title = stringResource(R.string.pref_library_update_refresh_trackers),
 | 
			
		||||
                    subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
 | 
			
		||||
                ),
 | 
			
		||||
                // TODO: remove isDevFlavor checks once functionality is available
 | 
			
		||||
                Preference.PreferenceItem.MultiSelectListPreference(
 | 
			
		||||
                    pref = libraryUpdateMangaRestrictionPref,
 | 
			
		||||
                    title = stringResource(R.string.pref_library_update_manga_restriction),
 | 
			
		||||
                    entries = buildMap {
 | 
			
		||||
                        put(MANGA_HAS_UNREAD, stringResource(R.string.pref_update_only_completely_read))
 | 
			
		||||
                        put(MANGA_NON_READ, stringResource(R.string.pref_update_only_started))
 | 
			
		||||
                        put(MANGA_NON_COMPLETED, stringResource(R.string.pref_update_only_non_completed))
 | 
			
		||||
                        if (isDevFlavor) {
 | 
			
		||||
                            put(MANGA_OUTSIDE_RELEASE_PERIOD, stringResource(R.string.pref_update_only_in_release_period))
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    entries = mapOf(
 | 
			
		||||
                        MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
 | 
			
		||||
                        MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
 | 
			
		||||
                        MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
 | 
			
		||||
                        MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                Preference.PreferenceItem.TextPreference(
 | 
			
		||||
                    title = stringResource(R.string.pref_update_release_grace_period),
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
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
 | 
			
		||||
@@ -12,10 +13,15 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
 | 
			
		||||
import kotlinx.coroutines.coroutineScope
 | 
			
		||||
import kotlinx.coroutines.isActive
 | 
			
		||||
import tachiyomi.domain.chapter.model.Chapter
 | 
			
		||||
import tachiyomi.domain.chapter.repository.ChapterRepository
 | 
			
		||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.domain.track.model.Track
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.time.ZonedDateTime
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +29,12 @@ class BackupRestorer(
 | 
			
		||||
    private val context: Context,
 | 
			
		||||
    private val notifier: BackupNotifier,
 | 
			
		||||
) {
 | 
			
		||||
    private val updateManga: UpdateManga = Injekt.get()
 | 
			
		||||
    private val chapterRepository: ChapterRepository = Injekt.get()
 | 
			
		||||
    private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
 | 
			
		||||
 | 
			
		||||
    private var zonedDateTime = ZonedDateTime.now()
 | 
			
		||||
    private var currentRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
 | 
			
		||||
 | 
			
		||||
    private var backupManager = BackupManager(context)
 | 
			
		||||
 | 
			
		||||
@@ -90,6 +102,8 @@ class BackupRestorer(
 | 
			
		||||
        // Store source mapping for error messages
 | 
			
		||||
        val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
 | 
			
		||||
        sourceMapping = backupMaps.associate { it.sourceId to it.name }
 | 
			
		||||
        zonedDateTime = ZonedDateTime.now()
 | 
			
		||||
        currentRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
 | 
			
		||||
 | 
			
		||||
        return coroutineScope {
 | 
			
		||||
            // Restore individual manga
 | 
			
		||||
@@ -122,7 +136,7 @@ class BackupRestorer(
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
 | 
			
		||||
            if (dbManga == null) {
 | 
			
		||||
            val restoredManga = if (dbManga == null) {
 | 
			
		||||
                // Manga not in database
 | 
			
		||||
                restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
 | 
			
		||||
            } else {
 | 
			
		||||
@@ -132,6 +146,8 @@ class BackupRestorer(
 | 
			
		||||
                // Fetch rest of manga information
 | 
			
		||||
                restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
 | 
			
		||||
            }
 | 
			
		||||
            val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id)
 | 
			
		||||
            updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentRange)
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
 | 
			
		||||
            errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
 | 
			
		||||
@@ -159,10 +175,11 @@ class BackupRestorer(
 | 
			
		||||
        history: List<BackupHistory>,
 | 
			
		||||
        tracks: List<Track>,
 | 
			
		||||
        backupCategories: List<BackupCategory>,
 | 
			
		||||
    ) {
 | 
			
		||||
    ): Manga {
 | 
			
		||||
        val fetchedManga = backupManager.restoreNewManga(manga)
 | 
			
		||||
        backupManager.restoreChapters(fetchedManga, chapters)
 | 
			
		||||
        restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
 | 
			
		||||
        return fetchedManga
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun restoreNewManga(
 | 
			
		||||
@@ -172,9 +189,10 @@ class BackupRestorer(
 | 
			
		||||
        history: List<BackupHistory>,
 | 
			
		||||
        tracks: List<Track>,
 | 
			
		||||
        backupCategories: List<BackupCategory>,
 | 
			
		||||
    ) {
 | 
			
		||||
    ): Manga {
 | 
			
		||||
        backupManager.restoreChapters(backupManga, chapters)
 | 
			
		||||
        restoreExtras(backupManga, categories, history, tracks, backupCategories)
 | 
			
		||||
        return backupManga
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
 | 
			
		||||
 
 | 
			
		||||
@@ -66,8 +66,10 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY
 | 
			
		||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_UNREAD
 | 
			
		||||
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.GetLibraryManga
 | 
			
		||||
import tachiyomi.domain.manga.interactor.GetManga
 | 
			
		||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.domain.manga.model.toMangaUpdate
 | 
			
		||||
import tachiyomi.domain.source.model.SourceNotInstalledException
 | 
			
		||||
@@ -77,6 +79,7 @@ import tachiyomi.domain.track.interactor.InsertTrack
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.time.ZonedDateTime
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import java.util.concurrent.CopyOnWriteArrayList
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
@@ -101,6 +104,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
 | 
			
		||||
    private val getTracks: GetTracks = Injekt.get()
 | 
			
		||||
    private val insertTrack: InsertTrack = Injekt.get()
 | 
			
		||||
    private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
 | 
			
		||||
    private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
 | 
			
		||||
 | 
			
		||||
    private val notifier = LibraryUpdateNotifier(context)
 | 
			
		||||
 | 
			
		||||
@@ -227,6 +231,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
 | 
			
		||||
        val hasDownloads = AtomicBoolean(false)
 | 
			
		||||
        val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
 | 
			
		||||
 | 
			
		||||
        val now = ZonedDateTime.now()
 | 
			
		||||
        val fetchRange = setMangaUpdateInterval.getCurrentFetchRange(now)
 | 
			
		||||
        val higherLimit = fetchRange.second
 | 
			
		||||
 | 
			
		||||
        coroutineScope {
 | 
			
		||||
            mangaToUpdate.groupBy { it.manga.source }.values
 | 
			
		||||
                .map { mangaInSource ->
 | 
			
		||||
@@ -247,6 +255,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
 | 
			
		||||
                                    manga,
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    when {
 | 
			
		||||
                                        MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit ->
 | 
			
		||||
                                            skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
 | 
			
		||||
 | 
			
		||||
                                        MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
 | 
			
		||||
                                            skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
 | 
			
		||||
 | 
			
		||||
@@ -261,7 +272,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
 | 
			
		||||
 | 
			
		||||
                                        else -> {
 | 
			
		||||
                                            try {
 | 
			
		||||
                                                val newChapters = updateManga(manga)
 | 
			
		||||
                                                val newChapters = updateManga(manga, now, fetchRange)
 | 
			
		||||
                                                    .sortedByDescending { it.sourceOrder }
 | 
			
		||||
 | 
			
		||||
                                                if (newChapters.isNotEmpty()) {
 | 
			
		||||
@@ -333,7 +344,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
 | 
			
		||||
     * @param manga the manga to update.
 | 
			
		||||
     * @return a pair of the inserted and removed chapters.
 | 
			
		||||
     */
 | 
			
		||||
    private suspend fun updateManga(manga: Manga): List<Chapter> {
 | 
			
		||||
    private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
 | 
			
		||||
        val source = sourceManager.getOrStub(manga.source)
 | 
			
		||||
 | 
			
		||||
        // Update manga metadata if needed
 | 
			
		||||
@@ -348,7 +359,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
 | 
			
		||||
        // to get latest data so it doesn't get overwritten later on
 | 
			
		||||
        val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
 | 
			
		||||
 | 
			
		||||
        return syncChaptersWithSource.await(chapters, dbManga, source)
 | 
			
		||||
        return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun updateCovers() {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ import eu.kanade.presentation.manga.EditCoverAction
 | 
			
		||||
import eu.kanade.presentation.manga.MangaScreen
 | 
			
		||||
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaCoverDialog
 | 
			
		||||
import eu.kanade.presentation.manga.components.SetIntervalDialog
 | 
			
		||||
import eu.kanade.presentation.util.AssistContentScreen
 | 
			
		||||
import eu.kanade.presentation.util.Screen
 | 
			
		||||
import eu.kanade.presentation.util.isTabletUi
 | 
			
		||||
@@ -53,6 +54,7 @@ import logcat.LogPriority
 | 
			
		||||
import tachiyomi.core.util.lang.withIOContext
 | 
			
		||||
import tachiyomi.core.util.system.logcat
 | 
			
		||||
import tachiyomi.domain.chapter.model.Chapter
 | 
			
		||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.presentation.core.screens.LoadingScreen
 | 
			
		||||
 | 
			
		||||
@@ -100,6 +102,7 @@ class MangaScreen(
 | 
			
		||||
            snackbarHostState = screenModel.snackbarHostState,
 | 
			
		||||
            dateRelativeTime = screenModel.relativeTime,
 | 
			
		||||
            dateFormat = screenModel.dateFormat,
 | 
			
		||||
            intervalDisplay = screenModel::intervalDisplay,
 | 
			
		||||
            isTabletUi = isTabletUi(),
 | 
			
		||||
            chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
 | 
			
		||||
            chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
 | 
			
		||||
@@ -121,7 +124,8 @@ class MangaScreen(
 | 
			
		||||
            onCoverClicked = screenModel::showCoverDialog,
 | 
			
		||||
            onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
 | 
			
		||||
            onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
 | 
			
		||||
            onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
 | 
			
		||||
            onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
 | 
			
		||||
            onEditIntervalClicked = screenModel::showSetMangaIntervalDialog.takeIf { MANGA_OUTSIDE_RELEASE_PERIOD in screenModel.libraryPreferences.libraryUpdateMangaRestriction().get() && successState.manga.favorite },
 | 
			
		||||
            onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
 | 
			
		||||
            onMultiBookmarkClicked = screenModel::bookmarkChapters,
 | 
			
		||||
            onMultiMarkAsReadClicked = screenModel::markChaptersRead,
 | 
			
		||||
@@ -207,6 +211,13 @@ class MangaScreen(
 | 
			
		||||
                    LoadingScreen(Modifier.systemBarsPadding())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is MangaScreenModel.Dialog.SetMangaInterval -> {
 | 
			
		||||
                SetIntervalDialog(
 | 
			
		||||
                    interval = if (dialog.manga.calculateInterval < 0) -dialog.manga.calculateInterval else 0,
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onValueChanged = { screenModel.setFetchRangeInterval(dialog.manga, it) },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -69,20 +69,22 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
 | 
			
		||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import tachiyomi.domain.manga.model.applyFilter
 | 
			
		||||
import tachiyomi.domain.manga.repository.MangaRepository
 | 
			
		||||
import tachiyomi.domain.source.service.SourceManager
 | 
			
		||||
import tachiyomi.domain.track.interactor.GetTracks
 | 
			
		||||
import tachiyomi.source.local.isLocal
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import kotlin.math.absoluteValue
 | 
			
		||||
 | 
			
		||||
class MangaScreenModel(
 | 
			
		||||
    val context: Context,
 | 
			
		||||
    val mangaId: Long,
 | 
			
		||||
    private val isFromSource: Boolean,
 | 
			
		||||
    private val downloadPreferences: DownloadPreferences = Injekt.get(),
 | 
			
		||||
    private val libraryPreferences: LibraryPreferences = Injekt.get(),
 | 
			
		||||
    readerPreferences: ReaderPreferences = Injekt.get(),
 | 
			
		||||
    uiPreferences: UiPreferences = Injekt.get(),
 | 
			
		||||
    val libraryPreferences: LibraryPreferences = Injekt.get(),
 | 
			
		||||
    val readerPreferences: ReaderPreferences = Injekt.get(),
 | 
			
		||||
    val uiPreferences: UiPreferences = Injekt.get(),
 | 
			
		||||
    private val trackManager: TrackManager = Injekt.get(),
 | 
			
		||||
    private val downloadManager: DownloadManager = Injekt.get(),
 | 
			
		||||
    private val downloadCache: DownloadCache = Injekt.get(),
 | 
			
		||||
@@ -97,6 +99,7 @@ class MangaScreenModel(
 | 
			
		||||
    private val getCategories: GetCategories = Injekt.get(),
 | 
			
		||||
    private val getTracks: GetTracks = Injekt.get(),
 | 
			
		||||
    private val setMangaCategories: SetMangaCategories = Injekt.get(),
 | 
			
		||||
    private val mangaRepository: MangaRepository = Injekt.get(),
 | 
			
		||||
    val snackbarHostState: SnackbarHostState = SnackbarHostState(),
 | 
			
		||||
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
 | 
			
		||||
 | 
			
		||||
@@ -307,7 +310,7 @@ class MangaScreenModel(
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Choose a category
 | 
			
		||||
                    else -> promptChangeCategories()
 | 
			
		||||
                    else -> showChangeCategoryDialog()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Finally match with enhanced tracking when available
 | 
			
		||||
@@ -333,7 +336,7 @@ class MangaScreenModel(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun promptChangeCategories() {
 | 
			
		||||
    fun showChangeCategoryDialog() {
 | 
			
		||||
        val manga = successState?.manga ?: return
 | 
			
		||||
        coroutineScope.launch {
 | 
			
		||||
            val categories = getCategories()
 | 
			
		||||
@@ -349,6 +352,39 @@ class MangaScreenModel(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun showSetMangaIntervalDialog() {
 | 
			
		||||
        val manga = successState?.manga ?: return
 | 
			
		||||
        updateSuccessState {
 | 
			
		||||
            it.copy(dialog = Dialog.SetMangaInterval(manga))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: this should be in the state/composables
 | 
			
		||||
    fun intervalDisplay(): Pair<Int, Int>? {
 | 
			
		||||
        val state = successState ?: return null
 | 
			
		||||
        val leadDay = libraryPreferences.leadingExpectedDays().get()
 | 
			
		||||
        val followDay = libraryPreferences.followingExpectedDays().get()
 | 
			
		||||
        val effInterval = state.manga.calculateInterval
 | 
			
		||||
        return 1.coerceAtLeast(effInterval.absoluteValue - leadDay) to (effInterval.absoluteValue + followDay)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setFetchRangeInterval(manga: Manga, newInterval: Int) {
 | 
			
		||||
        val interval = when (newInterval) {
 | 
			
		||||
            // reset interval 0 default to trigger recalculation
 | 
			
		||||
            // only reset if interval is custom, which is negative
 | 
			
		||||
            0 -> if (manga.calculateInterval < 0) 0 else manga.calculateInterval
 | 
			
		||||
            else -> -newInterval
 | 
			
		||||
        }
 | 
			
		||||
        coroutineScope.launchIO {
 | 
			
		||||
            updateManga.awaitUpdateFetchInterval(
 | 
			
		||||
                manga.copy(calculateInterval = interval),
 | 
			
		||||
                successState?.chapters?.map { it.chapter }.orEmpty(),
 | 
			
		||||
            )
 | 
			
		||||
            val newManga = mangaRepository.getMangaById(mangaId)
 | 
			
		||||
            updateSuccessState { it.copy(manga = newManga) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if the manga has any downloads.
 | 
			
		||||
     */
 | 
			
		||||
@@ -502,6 +538,7 @@ class MangaScreenModel(
 | 
			
		||||
                    chapters,
 | 
			
		||||
                    state.manga,
 | 
			
		||||
                    state.source,
 | 
			
		||||
                    manualFetch,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                if (manualFetch) {
 | 
			
		||||
@@ -519,6 +556,8 @@ class MangaScreenModel(
 | 
			
		||||
            coroutineScope.launch {
 | 
			
		||||
                snackbarHostState.showSnackbar(message = message)
 | 
			
		||||
            }
 | 
			
		||||
            val newManga = mangaRepository.getMangaById(mangaId)
 | 
			
		||||
            updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -943,6 +982,7 @@ class MangaScreenModel(
 | 
			
		||||
        data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
 | 
			
		||||
        data class DeleteChapters(val chapters: List<Chapter>) : Dialog
 | 
			
		||||
        data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
 | 
			
		||||
        data class SetMangaInterval(val manga: Manga) : Dialog
 | 
			
		||||
        data object SettingsSheet : Dialog
 | 
			
		||||
        data object TrackSheet : Dialog
 | 
			
		||||
        data object FullCover : Dialog
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user