diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt index 673b686f5e..ab75596d8f 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt @@ -48,6 +48,7 @@ class SetReadStatus( if (read && downloadPreferences.removeAfterMarkedAsRead().get()) { chaptersToUpdate + .filterNot { it.localChapter } .groupBy { it.mangaId } .forEach { (mangaId, chapters) -> deleteDownload.awaitAll( diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt index 225f0cb424..1f9fa40f62 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt @@ -80,7 +80,7 @@ class SyncChaptersWithSource( val toDelete = dbChapters.filterNot { dbChapter -> sourceChapters.any { sourceChapter -> dbChapter.url == sourceChapter.url - } + } || dbChapter.localChapter } val rightNow = Date().time diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt index af99f0e631..5cae1858ac 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt @@ -50,4 +50,5 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also { it.date_upload = dateUpload it.chapter_number = chapterNumber.toFloat() it.source_order = sourceOrder.toInt() + it.localChapter = localChapter } 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 740ece4a26..a82f43848f 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -52,6 +52,7 @@ import eu.kanade.domain.manga.model.chaptersFiltered import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ExpandableMangaDescription +import eu.kanade.presentation.manga.components.LocalChapterAction import eu.kanade.presentation.manga.components.MangaActionRow import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaChapterListItem @@ -96,6 +97,7 @@ fun MangaScreen( onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?, + onLocalChapter: ((List, LocalChapterAction) -> Unit)?, // For tags menu onTagSearch: (String) -> Unit, @@ -171,6 +173,7 @@ fun MangaScreen( onChapterSelected = onChapterSelected, onAllChapterSelected = onAllChapterSelected, onInvertSelection = onInvertSelection, + onLocalChapter = onLocalChapter, ) } else { MangaScreenLargeImpl( @@ -207,6 +210,7 @@ fun MangaScreen( onChapterSelected = onChapterSelected, onAllChapterSelected = onAllChapterSelected, onInvertSelection = onInvertSelection, + onLocalChapter = onLocalChapter, ) } } @@ -226,6 +230,7 @@ private fun MangaScreenSmallImpl( onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?, + onLocalChapter: ((List, LocalChapterAction) -> Unit)?, // For tags menu onTagSearch: (String) -> Unit, @@ -435,6 +440,7 @@ private fun MangaScreenSmallImpl( onDownloadChapter = onDownloadChapter, onChapterSelected = onChapterSelected, onChapterSwipe = onChapterSwipe, + onLocalChapter = onLocalChapter, ) } } @@ -457,6 +463,7 @@ fun MangaScreenLargeImpl( onWebViewClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?, + onLocalChapter: ((List, LocalChapterAction) -> Unit)?, // For tags menu onTagSearch: (String) -> Unit, @@ -658,6 +665,7 @@ fun MangaScreenLargeImpl( onDownloadChapter = onDownloadChapter, onChapterSelected = onChapterSelected, onChapterSwipe = onChapterSwipe, + onLocalChapter = onLocalChapter, ) } } @@ -719,6 +727,7 @@ private fun LazyListScope.sharedChapterItems( onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?, onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, + onLocalChapter: ((List, LocalChapterAction) -> Unit)?, ) { items( items = chapters, @@ -729,6 +738,7 @@ private fun LazyListScope.sharedChapterItems( val context = LocalContext.current MangaChapterListItem( + localChapter = chapterItem.chapter.localChapter, title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { stringResource( R.string.display_mode_chapter, @@ -779,6 +789,11 @@ private fun LazyListScope.sharedChapterItems( onChapterSwipe = { onChapterSwipe(chapterItem, it) }, + onLocalActionClick = if (onLocalChapter != null) { + { onLocalChapter(listOf(chapterItem), it) } + } else { + null + }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/LocalChapterComponents.kt b/app/src/main/java/eu/kanade/presentation/manga/components/LocalChapterComponents.kt new file mode 100644 index 0000000000..51d54ebea6 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/LocalChapterComponents.kt @@ -0,0 +1,60 @@ +package eu.kanade.presentation.manga.components + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.dp +import eu.kanade.presentation.components.DropdownMenu +import eu.kanade.tachiyomi.R +import tachiyomi.presentation.core.components.material.IconButtonTokens + +@Composable +fun LocalChapterIndicator( + modifier: Modifier = Modifier, + onClick: (LocalChapterAction) -> Unit, +) { + var isMenuExpanded by remember { mutableStateOf(false) } + Box( + modifier = modifier + .size(IconButtonTokens.StateLayerSize) + .combinedClickable( + onLongClick = { isMenuExpanded = true }, + onClick = { isMenuExpanded = true }, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.Folder, + contentDescription = null, + modifier = Modifier.size(26.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.action_delete)) }, + onClick = { + onClick(LocalChapterAction.DELETE) + isMenuExpanded = false + }, + ) + } + } +} + +enum class LocalChapterAction { + DELETE, +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt index 1fb0abb52d..7f2dfdc1bd 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt @@ -66,6 +66,7 @@ fun MangaChapterListItem( read: Boolean, bookmark: Boolean, selected: Boolean, + localChapter: Boolean, downloadIndicatorEnabled: Boolean, downloadStateProvider: () -> Download.State, downloadProgressProvider: () -> Int, @@ -75,6 +76,7 @@ fun MangaChapterListItem( onClick: () -> Unit, onDownloadClick: ((ChapterDownloadAction) -> Unit)?, onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, + onLocalActionClick: ((LocalChapterAction) -> Unit)?, ) { val haptic = LocalHapticFeedback.current val density = LocalDensity.current @@ -204,7 +206,7 @@ fun MangaChapterListItem( } } - if (onDownloadClick != null) { + if (onDownloadClick != null && !localChapter) { ChapterDownloadIndicator( enabled = downloadIndicatorEnabled, modifier = Modifier.padding(start = 4.dp), @@ -213,6 +215,12 @@ fun MangaChapterListItem( onClick = onDownloadClick, ) } + if (onLocalActionClick != null && localChapter) { + LocalChapterIndicator( + modifier = Modifier.padding(start = 4.dp), + onClick = onLocalActionClick, + ) + } } } } 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 84d9692472..4b4f446f10 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 @@ -23,6 +23,7 @@ import tachiyomi.presentation.core.components.WheelTextPicker fun DeleteChaptersDialog( onDismissRequest: () -> Unit, onConfirm: () -> Unit, + includeLocalChapter: Boolean, ) { AlertDialog( onDismissRequest = onDismissRequest, @@ -45,7 +46,15 @@ fun DeleteChaptersDialog( Text(text = stringResource(R.string.are_you_sure)) }, text = { - Text(text = stringResource(R.string.confirm_delete_chapters)) + Text( + text = stringResource( + if (includeLocalChapter) { + R.string.confirm_delete_user_chapters + } else { + R.string.confirm_delete_chapters + }, + ), + ) }, ) } 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 9c699f5353..1b514e0bfc 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 @@ -559,6 +559,7 @@ class BackupManager( chapter.sourceOrder, chapter.dateFetch, chapter.dateUpload, + chapter.localChapter, ) } } @@ -583,6 +584,7 @@ class BackupManager( dateFetch = null, dateUpload = null, chapterId = chapter.id, + localChapter = null, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt index 6d8cba4e2d..5885194ffe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt @@ -21,6 +21,7 @@ data class BackupChapter( @ProtoNumber(9) var chapterNumber: Float = 0F, @ProtoNumber(10) var sourceOrder: Long = 0, @ProtoNumber(11) var lastModifiedAt: Long = 0, + @ProtoNumber(12) var localChapter: Boolean = false, ) { fun toChapterImpl(): Chapter { return Chapter.create().copy( @@ -35,11 +36,12 @@ data class BackupChapter( dateUpload = this@BackupChapter.dateUpload, sourceOrder = this@BackupChapter.sourceOrder, lastModifiedAt = this@BackupChapter.lastModifiedAt, + localChapter = this@BackupChapter.localChapter, ) } } -val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Double, source_order: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long -> +val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Double, source_order: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long, localChapter: Boolean -> BackupChapter( url = url, name = name, @@ -52,5 +54,6 @@ val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlat dateUpload = dateUpload, sourceOrder = source_order, lastModifiedAt = lastModifiedAt, + localChapter = localChapter, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index 4ff50483ec..6bda1f88a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -21,6 +21,8 @@ interface Chapter : SChapter, Serializable { var source_order: Int var last_modified: Long + + var localChapter: Boolean } fun Chapter.toDomainChapter(): DomainChapter? { @@ -39,5 +41,6 @@ fun Chapter.toDomainChapter(): DomainChapter? { chapterNumber = chapter_number.toDouble(), scanlator = scanlator, lastModifiedAt = last_modified, + localChapter = localChapter, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt index 58ba41dec4..ef3695e054 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt @@ -28,6 +28,8 @@ class ChapterImpl : Chapter { override var last_modified: Long = 0 + override var localChapter: Boolean = false + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 184269ea0e..e2e4a9fc41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -24,6 +24,7 @@ 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.File /** * This class is used to manage chapter downloads in the application. It must be instantiated once @@ -353,6 +354,34 @@ class DownloadManager( } } + suspend fun moveChapters(oldSource: Source, oldManga: Manga, newSource: Source, newManga: Manga, chapters: List) { + val oldMangaDir = provider.getMangaDir(oldManga.title, oldSource) + val newMangaDir = provider.getMangaDir(newManga.title, newSource) + + if (oldMangaDir.exists() && oldMangaDir.isDirectory && + newMangaDir.exists() && newMangaDir.isDirectory + ) { + if (chapters.isNotEmpty()) { + for (chapter in chapters) { + val oldNames = provider.getValidChapterDirNames(chapter.name, chapter.scanlator) + val oldDownload = oldNames.asSequence() + .mapNotNull { oldMangaDir.findFile(it) } + .firstOrNull() ?: return + var name = provider.getChapterDirName(chapter.name, chapter.scanlator) + if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) { + name += ".cbz" + } + val destinationFile = File(String.format("%s/%s", newMangaDir.filePath!!, name)) + if (oldDownload.filePath != null) { + File(oldDownload.filePath!!).copyTo(destinationFile, false) + cache.addChapter(name, newMangaDir, newManga) + } + } + deleteChapters(chapters, oldManga, oldSource) + } + } + } + private suspend fun getChaptersToDelete(chapters: List, manga: Manga): List { // Retrieve the categories that are set to exclude from being deleted on read val categoriesToExclude = downloadPreferences.removeExcludeCategories().get().map(String::toLong) 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 71e40cee76..69f48a3702 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 @@ -51,6 +51,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.toChapterUpdate +import tachiyomi.domain.chapter.repository.ChapterRepository import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.source.service.SourceManager @@ -174,6 +175,7 @@ internal class MigrateDialogScreenModel( private val insertTrack: InsertTrack = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), private val preferenceStore: PreferenceStore = Injekt.get(), + private val chapterRepository: ChapterRepository = Injekt.get(), ) : StateScreenModel(State()) { val migrateFlags: Preference by lazy { @@ -292,6 +294,20 @@ internal class MigrateDialogScreenModel( if (oldSource != null) { downloadManager.deleteManga(oldManga, oldSource) } + } else if (replace) { + val downloadedChapters = getChapterByMangaId.await(oldManga.id).filter { chapter -> + downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, oldManga.title, oldManga.source) + } + val newChapters = downloadedChapters.map { chapter -> + chapter.copy( + name = "${chapter.name} (migrated)", + localChapter = true, + mangaId = newManga.id, + ) + } + downloadedChapters.zip(newChapters).forEach { pair -> downloadManager.renameChapter(oldSource!!, oldManga, pair.first, pair.second) } + chapterRepository.addAll(newChapters) + downloadManager.moveChapters(oldSource!!, oldManga, newSource, newManga, newChapters) } if (replace) { 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 8f56c4fcb9..5d3e85c5d0 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 @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.util.fastAny import androidx.core.net.toUri import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -107,6 +108,7 @@ class MangaScreen( onBackClicked = navigator::pop, onChapterClicked = { openChapter(context, it) }, onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() }, + onLocalChapter = screenModel::runLocalChapterActions.takeIf { successState.chapters.fastAny { it.chapter.localChapter } }, onAddToLibraryClicked = { screenModel.toggleFavorite() haptic.performHapticFeedback(HapticFeedbackType.LongPress) @@ -155,6 +157,7 @@ class MangaScreen( screenModel.toggleAllSelection(false) screenModel.deleteChapters(dialog.chapters) }, + includeLocalChapter = dialog.includeUserChapter, ) } is MangaScreenModel.Dialog.DuplicateManga -> DuplicateMangaDialog( 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 da1cd4ccdd..2ce529870d 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 @@ -6,6 +6,7 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.util.fastAny import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.coroutineScope import eu.kanade.core.preference.asState @@ -18,6 +19,7 @@ import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction +import eu.kanade.presentation.manga.components.LocalChapterAction import eu.kanade.presentation.util.formattedMessage import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.DownloadCache @@ -61,6 +63,7 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.chapter.model.NoChaptersException +import tachiyomi.domain.chapter.repository.ChapterRepository import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.service.LibraryPreferences @@ -99,6 +102,7 @@ class MangaScreenModel( private val getTracks: GetTracks = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(), private val mangaRepository: MangaRepository = Injekt.get(), + private val chapterRepository: ChapterRepository = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), ) : StateScreenModel(State.Loading) { @@ -196,6 +200,7 @@ class MangaScreenModel( val fetchFromSourceTasks = listOf( async { if (needRefreshInfo) fetchMangaFromSource() }, async { if (needRefreshChapter) fetchChaptersFromSource() }, + async { if (needRefreshChapter) checkUserChaptersFromDeletion() }, ) fetchFromSourceTasks.awaitAll() } @@ -211,12 +216,24 @@ class MangaScreenModel( val fetchFromSourceTasks = listOf( async { fetchMangaFromSource(manualFetch) }, async { fetchChaptersFromSource(manualFetch) }, + async { checkUserChaptersFromDeletion() }, ) fetchFromSourceTasks.awaitAll() updateSuccessState { it.copy(isRefreshingData = false) } } } + private suspend fun checkUserChaptersFromDeletion() { + val state = successState ?: return + val manga = state.manga + val toDeleteIds = state.chapters.map { + it.chapter + }.filter { chapter -> + chapter.localChapter && + !downloadCache.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source, true) + }.map { it.id } + chapterRepository.removeChaptersWithIds(toDeleteIds) + } // Manga info - start /** @@ -692,6 +709,24 @@ class MangaScreenModel( if (pointerPos != -1) markChaptersRead(prevChapters.take(pointerPos), true) } + fun runLocalChapterActions( + items: List, + action: LocalChapterAction, + ) { + when (action) { + LocalChapterAction.DELETE -> { + updateSuccessState { successState -> + successState.copy( + dialog = Dialog.DeleteChapters( + chapters = items.map { it.chapter }, + includeUserChapter = true, + ), + ) + } + } + } + } + /** * Mark the selected chapter list as read/unread. * @param chapters the list of selected chapters. @@ -746,6 +781,8 @@ class MangaScreenModel( state.source, ) } + val toDeleteUserChaptersIds = chapters.filter { it.localChapter }.map { it.id } + chapterRepository.removeChaptersWithIds(toDeleteUserChaptersIds) } catch (e: Throwable) { logcat(LogPriority.ERROR, e) } @@ -965,7 +1002,7 @@ class MangaScreenModel( sealed interface Dialog { data class ChangeCategory(val manga: Manga, val initialSelection: List>) : Dialog - data class DeleteChapters(val chapters: List) : Dialog + data class DeleteChapters(val chapters: List, val includeUserChapter: Boolean) : Dialog data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog data class SetFetchInterval(val manga: Manga) : Dialog data object SettingsSheet : Dialog @@ -978,7 +1015,7 @@ class MangaScreenModel( } fun showDeleteChapterDialog(chapters: List) { - updateSuccessState { it.copy(dialog = Dialog.DeleteChapters(chapters)) } + updateSuccessState { it.copy(dialog = Dialog.DeleteChapters(chapters, chapters.fastAny { chapter -> chapter.localChapter })) } } fun showSettingsDialog() { 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 7da721c58f..f0d440f9d6 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 @@ -468,7 +468,7 @@ class ReaderViewModel( // Determine which chapter should be deleted and enqueue val currentChapterPosition = chapterList.indexOf(currentChapter) - val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots) + val chapterToDelete = chapterList.filterNot { it.chapter.localChapter }.getOrNull(currentChapterPosition - removeAfterReadSlots) // If chapter is completely read, no need to download it chapterToDownload = null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index d015a29b7d..47011c68fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -5,6 +5,7 @@ import com.github.junrar.exception.UnsupportedRarV5Exception import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadProvider +import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter @@ -57,6 +58,11 @@ class ChapterLoader( chapter.state = ReaderChapter.State.Loaded(pages) } catch (e: Throwable) { + if (e is HttpException && chapter.chapter.localChapter) { + val localChapterException = Exception(context.getString(R.string.local_chapter_not_found)) + chapter.state = ReaderChapter.State.Error(localChapterException) + throw localChapterException + } chapter.state = ReaderChapter.State.Error(e) throw e } diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt index 71ae1720ee..7dc5681803 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterMapper.kt @@ -2,8 +2,8 @@ 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 = - { id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt -> +val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Double, Long, Long, Long, Long, Boolean) -> Chapter = + { id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt, localChapter -> Chapter( id = id, mangaId = mangaId, @@ -18,5 +18,6 @@ val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, chapterNumber = chapterNumber, scanlator = scanlator, lastModifiedAt = lastModifiedAt, + localChapter = localChapter, ) } diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt index a5c462d777..848d39afae 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt @@ -28,6 +28,7 @@ class ChapterRepositoryImpl( chapter.sourceOrder, chapter.dateFetch, chapter.dateUpload, + chapter.localChapter, ) val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne() chapter.copy(id = lastInsertId) @@ -63,6 +64,7 @@ class ChapterRepositoryImpl( dateFetch = chapterUpdate.dateFetch, dateUpload = chapterUpdate.dateUpload, chapterId = chapterUpdate.id, + localChapter = chapterUpdate.localChapter, ) } } diff --git a/data/src/main/sqldelight/tachiyomi/data/chapters.sq b/data/src/main/sqldelight/tachiyomi/data/chapters.sq index ef22228853..508414d287 100644 --- a/data/src/main/sqldelight/tachiyomi/data/chapters.sq +++ b/data/src/main/sqldelight/tachiyomi/data/chapters.sq @@ -14,6 +14,7 @@ CREATE TABLE chapters( date_fetch INTEGER NOT NULL, date_upload INTEGER NOT NULL, last_modified_at INTEGER NOT NULL DEFAULT 0, + local_chapter INTEGER AS Boolean NOT NULL DEFAULT 0, FOREIGN KEY(manga_id) REFERENCES mangas (_id) ON DELETE CASCADE ); @@ -62,8 +63,8 @@ DELETE FROM chapters WHERE _id IN :chapterIds; insert: -INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at) -VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, strftime('%s', 'now')); +INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, local_chapter) +VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, strftime('%s', 'now'), :localChapter); update: UPDATE chapters @@ -77,7 +78,8 @@ SET manga_id = coalesce(:mangaId, manga_id), chapter_number = coalesce(:chapterNumber, chapter_number), source_order = coalesce(:sourceOrder, source_order), date_fetch = coalesce(:dateFetch, date_fetch), - date_upload = coalesce(:dateUpload, date_upload) + date_upload = coalesce(:dateUpload, date_upload), + local_chapter = coalesce(:localChapter, local_chapter) WHERE _id = :chapterId; selectLastInsertedRowId: diff --git a/data/src/main/sqldelight/tachiyomi/migrations/26.sqm b/data/src/main/sqldelight/tachiyomi/migrations/26.sqm new file mode 100644 index 0000000000..a5289fe941 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/26.sqm @@ -0,0 +1,3 @@ +ALTER TABLE chapters ADD COLUMN local_chapter INTEGER AS Boolean NOT NULL DEFAULT 0; + +UPDATE chapters SET local_chapter = 0; \ No newline at end of file diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt index 0029d67ead..58c15ff37c 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt @@ -14,6 +14,7 @@ data class Chapter( val chapterNumber: Double, val scanlator: String?, val lastModifiedAt: Long, + val localChapter: Boolean, ) { val isRecognizedNumber: Boolean get() = chapterNumber >= 0f @@ -33,6 +34,7 @@ data class Chapter( chapterNumber = -1.0, scanlator = null, lastModifiedAt = 0, + localChapter = false, ) } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt index 58cc88821e..58acb578f1 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt @@ -13,8 +13,9 @@ data class ChapterUpdate( val dateUpload: Long? = null, val chapterNumber: Double? = null, val scanlator: String? = null, + val localChapter: Boolean? = null, ) fun Chapter.toChapterUpdate(): ChapterUpdate { - return ChapterUpdate(id, mangaId, read, bookmark, lastPageRead, dateFetch, sourceOrder, url, name, dateUpload, chapterNumber, scanlator) + return ChapterUpdate(id, mangaId, read, bookmark, lastPageRead, dateFetch, sourceOrder, url, name, dateUpload, chapterNumber, scanlator, localChapter) } diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 596de0f941..68526af1c4 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -688,6 +688,7 @@ Error saving cover Error sharing cover Are you sure you want to delete the selected chapters? + Are you sure you want to delete the selected chapters? They include local files that may be irretrievably lost after deletion. Chapter settings Are you sure you want to save these settings as default? Also apply to all entries in my library @@ -775,6 +776,7 @@ Loading pages… Failed to load pages: %1$s No pages found + Local chapter file not found Source not found RARv5 format is not supported