diff --git a/app/src/main/java/eu/kanade/presentation/library/DeleteLibraryMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/library/DeleteLibraryMangaDialog.kt index da6faa495..9d4c7c98e 100644 --- a/app/src/main/java/eu/kanade/presentation/library/DeleteLibraryMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/DeleteLibraryMangaDialog.kt @@ -57,17 +57,14 @@ fun DeleteLibraryMangaDialog( }, text = { Column { - list.forEach { state -> + list.forEachIndexed { index, state -> LabeledCheckbox( label = stringResource(state.value), checked = state.isChecked, onCheckedChange = { - val index = list.indexOf(state) - if (index != -1) { - val mutableList = list.toMutableList() - mutableList[index] = state.next() as CheckboxState.State - list = mutableList.toList() - } + val mutableList = list.toMutableList() + mutableList[index] = state.next() as CheckboxState.State + list = mutableList.toList() }, ) } 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 1d323f599..4a43e25fe 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -201,6 +201,7 @@ fun MangaScreen( // KMK --> getMangaState: @Composable (Manga) -> State, onClickSourceSettingsClicked: (() -> Unit)?, + onClearManga: () -> Unit, onOpenMangaFolder: (() -> Unit)?, onRelatedMangasScreenClick: () -> Unit, onRelatedMangaClick: (Manga) -> Unit, @@ -269,6 +270,7 @@ fun MangaScreen( // KMK --> getMangaState = getMangaState, onClickSourceSettingsClicked = onClickSourceSettingsClicked, + onClearManga = onClearManga, onOpenMangaFolder = onOpenMangaFolder, onRelatedMangasScreenClick = onRelatedMangasScreenClick, onRelatedMangaClick = onRelatedMangaClick, @@ -330,6 +332,7 @@ fun MangaScreen( // KMK --> getMangaState = getMangaState, onClickSourceSettingsClicked = onClickSourceSettingsClicked, + onClearManga = onClearManga, onOpenMangaFolder = onOpenMangaFolder, onRelatedMangasScreenClick = onRelatedMangasScreenClick, onRelatedMangaClick = onRelatedMangaClick, @@ -408,6 +411,7 @@ private fun MangaScreenSmallImpl( // KMK --> getMangaState: @Composable ((Manga) -> State), onClickSourceSettingsClicked: (() -> Unit)?, + onClearManga: () -> Unit, onOpenMangaFolder: (() -> Unit)?, onRelatedMangasScreenClick: () -> Unit, onRelatedMangaClick: (Manga) -> Unit, @@ -491,6 +495,7 @@ private fun MangaScreenSmallImpl( onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite }, // KMK --> onClickSourceSettings = onClickSourceSettingsClicked, + onClearManga = onClearManga, onOpenMangaFolder = onOpenMangaFolder, onClickRelatedMangas = onRelatedMangasScreenClick.takeIf { !expandRelatedMangas && @@ -875,6 +880,7 @@ private fun MangaScreenLargeImpl( // KMK --> getMangaState: @Composable ((Manga) -> State), onClickSourceSettingsClicked: (() -> Unit)?, + onClearManga: () -> Unit, onOpenMangaFolder: (() -> Unit)?, onRelatedMangasScreenClick: () -> Unit, onRelatedMangaClick: (Manga) -> Unit, @@ -950,6 +956,7 @@ private fun MangaScreenLargeImpl( onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite }, // KMK --> onClickSourceSettings = onClickSourceSettingsClicked, + onClearManga = onClearManga, onOpenMangaFolder = onOpenMangaFolder, onClickRelatedMangas = onRelatedMangasScreenClick.takeIf { !expandRelatedMangas && 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 3f14a7e7a..a714e48c2 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 @@ -12,6 +12,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -19,10 +20,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.util.system.isReleaseBuildType import kotlinx.collections.immutable.toImmutableList +import tachiyomi.core.common.preference.CheckboxState import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.i18n.MR +import tachiyomi.i18n.kmk.KMR +import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.pluralStringResource @@ -62,6 +67,65 @@ fun DeleteChaptersDialog( ) } +// KMK --> +@Composable +fun ClearMangaDialog( + isMergedSource: Boolean, + onDismissRequest: () -> Unit, + onConfirm: (Boolean, Boolean) -> Unit, +) { + var list by remember { + mutableStateOf( + buildList> { + add(CheckboxState.State.None(KMR.strings.downloaded_data)) + add(CheckboxState.State.None(KMR.strings.chapters_from_database)) + }, + ) + } + AlertDialog( + onDismissRequest = onDismissRequest, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + confirmButton = { + TextButton( + enabled = list.any { it.isChecked }, + onClick = { + onDismissRequest() + onConfirm( + list[0].isChecked, + list[1].isChecked, + ) + }, + ) { + Text(text = stringResource(MR.strings.action_ok)) + } + }, + title = { + Text(text = stringResource(MR.strings.action_remove)) + }, + text = { + Column { + list.forEachIndexed { index, state -> + LabeledCheckbox( + label = stringResource(state.value), + checked = state.isChecked, + enabled = index != 0 || !isMergedSource, + onCheckedChange = { + val mutableList = list.toMutableList() + mutableList[index] = state.next() as CheckboxState.State + list = mutableList.toList() + }, + ) + } + } + }, + ) +} +// KMK <-- + @Composable fun SetIntervalDialog( interval: Int, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt index bd8b89c08..b76c7ca9b 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt @@ -53,6 +53,7 @@ fun MangaToolbar( // KMK --> onClickRelatedMangas: (() -> Unit)?, onClickSourceSettings: (() -> Unit)?, + onClearManga: () -> Unit, onOpenMangaFolder: (() -> Unit)?, // KMK <-- onClickRecommend: (() -> Unit)?, @@ -243,6 +244,12 @@ fun MangaToolbar( ), ) } + add( + AppBar.OverflowAction( + title = stringResource(KMR.strings.action_clear_manga), + onClick = onClearManga, + ), + ) if (onClickSourceSettings != null) { add( AppBar.OverflowAction( 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 a02085989..8797bf1f1 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 @@ -27,6 +27,7 @@ import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.service.SourceManager import tachiyomi.i18n.MR +import tachiyomi.source.local.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -277,6 +278,10 @@ class DownloadManager( provider.findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)?.delete() cache.removeManga(manga) + // KMK --> + if (source.isLocal()) return@launchIO + // KMK <-- + // Delete source directory if empty val sourceDir = provider.findSourceDir(source) if (sourceDir?.listFiles()?.isEmpty() == true) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index fb9f2cac0..7aec5ac86 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -12,6 +12,7 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.storage.service.StorageManager import tachiyomi.i18n.MR +import tachiyomi.source.local.isLocal import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.IOException @@ -74,7 +75,13 @@ class DownloadProvider( * @param source the source to query. */ fun findSourceDir(source: Source): UniFile? { - return downloadsDir?.findFile(getSourceDirName(source)) + // KMK --> + return if (source.isLocal()) { + storageManager.getLocalSourceDirectory() + } else { + // KMK <-- + downloadsDir?.findFile(getSourceDirName(source)) + } } /** @@ -113,9 +120,22 @@ class DownloadProvider( fun findChapterDirs(chapters: List, manga: Manga, source: Source): Pair> { val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return null to emptyList() return mangaDir to chapters.mapNotNull { chapter -> - getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence() - .mapNotNull { mangaDir.findFile(it) } - .firstOrNull() + // KMK --> + if (source.isLocal()) { + val splitUrl = chapter.url.split('/', limit = 2) + if (splitUrl.size < 2) { + null + } else { + val (mangaDirName, chapterDirName) = splitUrl + mangaDir.findFile(chapterDirName) + ?: storageManager.getLocalSourceDirectory()?.findFile(mangaDirName)?.findFile(chapterDirName) + } + } else { + // KMK <-- + getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence() + .mapNotNull { mangaDir.findFile(it) } + .firstOrNull() + } } } 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 85e57dc42..44425c2b8 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 @@ -52,6 +52,7 @@ import eu.kanade.presentation.manga.ChapterSettingsDialog import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.manga.EditCoverAction import eu.kanade.presentation.manga.MangaScreen +import eu.kanade.presentation.manga.components.ClearMangaDialog import eu.kanade.presentation.manga.components.DeleteChaptersDialog import eu.kanade.presentation.manga.components.MangaCoverDialog import eu.kanade.presentation.manga.components.ScanlatorFilterDialog @@ -65,6 +66,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.isLocalOrStub import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreen import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen @@ -418,8 +420,9 @@ class MangaScreen( else -> {} } }.takeIf { isConfigurableSource }, + onClearManga = { screenModel.showClearMangaDialog(successState.source is MergedSource) }, onOpenMangaFolder = { screenModel.openMangaFolder() } - .takeIf { successState.source !is StubSource }, + .takeIf { successState.source !is StubSource && successState.source !is MergedSource }, onRelatedMangasScreenClick = { if (successState.isRelatedMangasFetched == null) { scope.launchIO { screenModel.fetchRelatedMangasFromSource(onDemand = true) } @@ -634,6 +637,15 @@ class MangaScreen( ) } // SY <-- + // KMK --> + is MangaScreenModel.Dialog.ClearManga -> { + ClearMangaDialog( + isMergedSource = dialog.isMergedSource, + onDismissRequest = onDismissRequest, + onConfirm = screenModel::clearManga, + ) + } + // KMK <-- } if (showScanlatorsDialog) { 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 0894db0cd..0b04421bc 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 @@ -120,6 +120,7 @@ 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 +import tachiyomi.domain.chapter.interactor.DeleteChapters import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.chapter.interactor.UpdateChapter @@ -231,6 +232,7 @@ class MangaScreenModel( private val deleteLibraryUpdateErrors: DeleteLibraryUpdateErrors = Injekt.get(), private val insertLibraryUpdateErrors: InsertLibraryUpdateErrors = Injekt.get(), private val insertLibraryUpdateErrorMessages: InsertLibraryUpdateErrorMessages = Injekt.get(), + private val deleteChaptersFromDb: DeleteChapters = Injekt.get(), // KMK <-- ) : StateScreenModel(State.Loading) { @@ -1524,6 +1526,12 @@ class MangaScreenModel( ignoreCategoryExclusion = true, // KMK <-- ) + // KMK --> + if (source?.isLocal() == true) { + // Refresh chapters state for Local source + fetchChaptersFromSource() + } + // KMK <-- } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) @@ -1531,6 +1539,51 @@ class MangaScreenModel( } } + // KMK --> + fun clearManga( + deleteDownload: Boolean, + removeChapters: Boolean, + ) { + if (deleteDownload) { + deleteDownloadedData() + } + if (removeChapters) { + removeChaptersDatabase() + } + } + + private fun deleteDownloadedData() { + screenModelScope.launchNonCancellable { + try { + successState?.let { state -> + downloadManager.deleteManga( + manga = state.manga, + source = state.source, + removeQueued = true, + ) + if (source?.isLocal() == true) { + // Refresh chapters state for Local source + fetchChaptersFromSource() + } + } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) + } + } + } + + private fun removeChaptersDatabase() { + screenModelScope.launchNonCancellable { + try { + successState?.chapters?.map { it.id } + ?.let { deleteChaptersFromDb.await(it) } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) + } + } + } + // KMK <-- + private fun downloadNewChapters(chapters: List) { screenModelScope.launchNonCancellable { val manga = successState?.manga ?: return@launchNonCancellable @@ -1822,6 +1875,10 @@ class MangaScreenModel( data class EditMergedSettings(val mergedData: MergedMangaData) : Dialog // SY <-- + // KMK --> + data class ClearManga(val isMergedSource: Boolean) : Dialog + // KMK <-- + data object SettingsSheet : Dialog data object TrackSheet : Dialog data object FullCover : Dialog @@ -1883,6 +1940,12 @@ class MangaScreenModel( } // SY <-- + // KMK --> + fun showClearMangaDialog(isMergedSource: Boolean) { + updateSuccessState { it.copy(dialog = Dialog.ClearManga(isMergedSource)) } + } + // KMK <-- + sealed interface State { @Immutable data object Loading : State diff --git a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml index adf211723..e9ef688fd 100644 --- a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml @@ -15,7 +15,9 @@ Show hidden categories Panorama cover Open folder + Clear manga Update + Copy first page to clipboard @@ -121,6 +123,9 @@ Skip it Allow all Skip all + Downloaded data + Chapters & History from Database + Try now Update completed