Add localChapter

This commit is contained in:
semenvav 2023-08-10 16:52:41 +03:00
parent 81cd765543
commit 50faf3d437
24 changed files with 221 additions and 13 deletions

View file

@ -48,6 +48,7 @@ class SetReadStatus(
if (read && downloadPreferences.removeAfterMarkedAsRead().get()) { if (read && downloadPreferences.removeAfterMarkedAsRead().get()) {
chaptersToUpdate chaptersToUpdate
.filterNot { it.localChapter }
.groupBy { it.mangaId } .groupBy { it.mangaId }
.forEach { (mangaId, chapters) -> .forEach { (mangaId, chapters) ->
deleteDownload.awaitAll( deleteDownload.awaitAll(

View file

@ -80,7 +80,7 @@ class SyncChaptersWithSource(
val toDelete = dbChapters.filterNot { dbChapter -> val toDelete = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter -> sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url dbChapter.url == sourceChapter.url
} } || dbChapter.localChapter
} }
val rightNow = Date().time val rightNow = Date().time

View file

@ -50,4 +50,5 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
it.date_upload = dateUpload it.date_upload = dateUpload
it.chapter_number = chapterNumber.toFloat() it.chapter_number = chapterNumber.toFloat()
it.source_order = sourceOrder.toInt() it.source_order = sourceOrder.toInt()
it.localChapter = localChapter
} }

View file

@ -52,6 +52,7 @@ import eu.kanade.domain.manga.model.chaptersFiltered
import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription 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.MangaActionRow
import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaBottomActionMenu
import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaChapterListItem
@ -96,6 +97,7 @@ fun MangaScreen(
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?,
onLocalChapter: ((List<ChapterItem>, LocalChapterAction) -> Unit)?,
// For tags menu // For tags menu
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
@ -171,6 +173,7 @@ fun MangaScreen(
onChapterSelected = onChapterSelected, onChapterSelected = onChapterSelected,
onAllChapterSelected = onAllChapterSelected, onAllChapterSelected = onAllChapterSelected,
onInvertSelection = onInvertSelection, onInvertSelection = onInvertSelection,
onLocalChapter = onLocalChapter,
) )
} else { } else {
MangaScreenLargeImpl( MangaScreenLargeImpl(
@ -207,6 +210,7 @@ fun MangaScreen(
onChapterSelected = onChapterSelected, onChapterSelected = onChapterSelected,
onAllChapterSelected = onAllChapterSelected, onAllChapterSelected = onAllChapterSelected,
onInvertSelection = onInvertSelection, onInvertSelection = onInvertSelection,
onLocalChapter = onLocalChapter,
) )
} }
} }
@ -226,6 +230,7 @@ private fun MangaScreenSmallImpl(
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?,
onLocalChapter: ((List<ChapterItem>, LocalChapterAction) -> Unit)?,
// For tags menu // For tags menu
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
@ -435,6 +440,7 @@ private fun MangaScreenSmallImpl(
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected, onChapterSelected = onChapterSelected,
onChapterSwipe = onChapterSwipe, onChapterSwipe = onChapterSwipe,
onLocalChapter = onLocalChapter,
) )
} }
} }
@ -457,6 +463,7 @@ fun MangaScreenLargeImpl(
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?,
onLocalChapter: ((List<ChapterItem>, LocalChapterAction) -> Unit)?,
// For tags menu // For tags menu
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
@ -658,6 +665,7 @@ fun MangaScreenLargeImpl(
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected, onChapterSelected = onChapterSelected,
onChapterSwipe = onChapterSwipe, onChapterSwipe = onChapterSwipe,
onLocalChapter = onLocalChapter,
) )
} }
} }
@ -719,6 +727,7 @@ private fun LazyListScope.sharedChapterItems(
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit, onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
onLocalChapter: ((List<ChapterItem>, LocalChapterAction) -> Unit)?,
) { ) {
items( items(
items = chapters, items = chapters,
@ -729,6 +738,7 @@ private fun LazyListScope.sharedChapterItems(
val context = LocalContext.current val context = LocalContext.current
MangaChapterListItem( MangaChapterListItem(
localChapter = chapterItem.chapter.localChapter,
title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) { title = if (manga.displayMode == Manga.CHAPTER_DISPLAY_NUMBER) {
stringResource( stringResource(
R.string.display_mode_chapter, R.string.display_mode_chapter,
@ -779,6 +789,11 @@ private fun LazyListScope.sharedChapterItems(
onChapterSwipe = { onChapterSwipe = {
onChapterSwipe(chapterItem, it) onChapterSwipe(chapterItem, it)
}, },
onLocalActionClick = if (onLocalChapter != null) {
{ onLocalChapter(listOf(chapterItem), it) }
} else {
null
},
) )
} }
} }

View file

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

View file

@ -66,6 +66,7 @@ fun MangaChapterListItem(
read: Boolean, read: Boolean,
bookmark: Boolean, bookmark: Boolean,
selected: Boolean, selected: Boolean,
localChapter: Boolean,
downloadIndicatorEnabled: Boolean, downloadIndicatorEnabled: Boolean,
downloadStateProvider: () -> Download.State, downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int, downloadProgressProvider: () -> Int,
@ -75,6 +76,7 @@ fun MangaChapterListItem(
onClick: () -> Unit, onClick: () -> Unit,
onDownloadClick: ((ChapterDownloadAction) -> Unit)?, onDownloadClick: ((ChapterDownloadAction) -> Unit)?,
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
onLocalActionClick: ((LocalChapterAction) -> Unit)?,
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val density = LocalDensity.current val density = LocalDensity.current
@ -204,7 +206,7 @@ fun MangaChapterListItem(
} }
} }
if (onDownloadClick != null) { if (onDownloadClick != null && !localChapter) {
ChapterDownloadIndicator( ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled, enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp),
@ -213,6 +215,12 @@ fun MangaChapterListItem(
onClick = onDownloadClick, onClick = onDownloadClick,
) )
} }
if (onLocalActionClick != null && localChapter) {
LocalChapterIndicator(
modifier = Modifier.padding(start = 4.dp),
onClick = onLocalActionClick,
)
}
} }
} }
} }

View file

@ -23,6 +23,7 @@ import tachiyomi.presentation.core.components.WheelTextPicker
fun DeleteChaptersDialog( fun DeleteChaptersDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
includeLocalChapter: Boolean,
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -45,7 +46,15 @@ fun DeleteChaptersDialog(
Text(text = stringResource(R.string.are_you_sure)) Text(text = stringResource(R.string.are_you_sure))
}, },
text = { 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
},
),
)
}, },
) )
} }

View file

@ -559,6 +559,7 @@ class BackupManager(
chapter.sourceOrder, chapter.sourceOrder,
chapter.dateFetch, chapter.dateFetch,
chapter.dateUpload, chapter.dateUpload,
chapter.localChapter,
) )
} }
} }
@ -583,6 +584,7 @@ class BackupManager(
dateFetch = null, dateFetch = null,
dateUpload = null, dateUpload = null,
chapterId = chapter.id, chapterId = chapter.id,
localChapter = null,
) )
} }
} }

View file

@ -21,6 +21,7 @@ data class BackupChapter(
@ProtoNumber(9) var chapterNumber: Float = 0F, @ProtoNumber(9) var chapterNumber: Float = 0F,
@ProtoNumber(10) var sourceOrder: Long = 0, @ProtoNumber(10) var sourceOrder: Long = 0,
@ProtoNumber(11) var lastModifiedAt: Long = 0, @ProtoNumber(11) var lastModifiedAt: Long = 0,
@ProtoNumber(12) var localChapter: Boolean = false,
) { ) {
fun toChapterImpl(): Chapter { fun toChapterImpl(): Chapter {
return Chapter.create().copy( return Chapter.create().copy(
@ -35,11 +36,12 @@ data class BackupChapter(
dateUpload = this@BackupChapter.dateUpload, dateUpload = this@BackupChapter.dateUpload,
sourceOrder = this@BackupChapter.sourceOrder, sourceOrder = this@BackupChapter.sourceOrder,
lastModifiedAt = this@BackupChapter.lastModifiedAt, 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( BackupChapter(
url = url, url = url,
name = name, name = name,
@ -52,5 +54,6 @@ val backupChapterMapper = { _: Long, _: Long, url: String, name: String, scanlat
dateUpload = dateUpload, dateUpload = dateUpload,
sourceOrder = source_order, sourceOrder = source_order,
lastModifiedAt = lastModifiedAt, lastModifiedAt = lastModifiedAt,
localChapter = localChapter,
) )
} }

View file

@ -21,6 +21,8 @@ interface Chapter : SChapter, Serializable {
var source_order: Int var source_order: Int
var last_modified: Long var last_modified: Long
var localChapter: Boolean
} }
fun Chapter.toDomainChapter(): DomainChapter? { fun Chapter.toDomainChapter(): DomainChapter? {
@ -39,5 +41,6 @@ fun Chapter.toDomainChapter(): DomainChapter? {
chapterNumber = chapter_number.toDouble(), chapterNumber = chapter_number.toDouble(),
scanlator = scanlator, scanlator = scanlator,
lastModifiedAt = last_modified, lastModifiedAt = last_modified,
localChapter = localChapter,
) )
} }

View file

@ -28,6 +28,8 @@ class ChapterImpl : Chapter {
override var last_modified: Long = 0 override var last_modified: Long = 0
override var localChapter: Boolean = false
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false

View file

@ -24,6 +24,7 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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 * 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<Chapter>) {
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<Chapter>, manga: Manga): List<Chapter> { private suspend fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
// Retrieve the categories that are set to exclude from being deleted on read // Retrieve the categories that are set to exclude from being deleted on read
val categoriesToExclude = downloadPreferences.removeExcludeCategories().get().map(String::toLong) val categoriesToExclude = downloadPreferences.removeExcludeCategories().get().map(String::toLong)

View file

@ -51,6 +51,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@ -174,6 +175,7 @@ internal class MigrateDialogScreenModel(
private val insertTrack: InsertTrack = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val preferenceStore: PreferenceStore = Injekt.get(), private val preferenceStore: PreferenceStore = Injekt.get(),
private val chapterRepository: ChapterRepository = Injekt.get(),
) : StateScreenModel<MigrateDialogScreenModel.State>(State()) { ) : StateScreenModel<MigrateDialogScreenModel.State>(State()) {
val migrateFlags: Preference<Int> by lazy { val migrateFlags: Preference<Int> by lazy {
@ -292,6 +294,20 @@ internal class MigrateDialogScreenModel(
if (oldSource != null) { if (oldSource != null) {
downloadManager.deleteManga(oldManga, oldSource) 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) { if (replace) {

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.util.fastAny
import androidx.core.net.toUri import androidx.core.net.toUri
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -107,6 +108,7 @@ class MangaScreen(
onBackClicked = navigator::pop, onBackClicked = navigator::pop,
onChapterClicked = { openChapter(context, it) }, onChapterClicked = { openChapter(context, it) },
onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() }, onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() },
onLocalChapter = screenModel::runLocalChapterActions.takeIf { successState.chapters.fastAny { it.chapter.localChapter } },
onAddToLibraryClicked = { onAddToLibraryClicked = {
screenModel.toggleFavorite() screenModel.toggleFavorite()
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@ -155,6 +157,7 @@ class MangaScreen(
screenModel.toggleAllSelection(false) screenModel.toggleAllSelection(false)
screenModel.deleteChapters(dialog.chapters) screenModel.deleteChapters(dialog.chapters)
}, },
includeLocalChapter = dialog.includeUserChapter,
) )
} }
is MangaScreenModel.Dialog.DuplicateManga -> DuplicateMangaDialog( is MangaScreenModel.Dialog.DuplicateManga -> DuplicateMangaDialog(

View file

@ -6,6 +6,7 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.util.fastAny
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.preference.asState 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.domain.ui.UiPreferences
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.LocalChapterAction
import eu.kanade.presentation.util.formattedMessage import eu.kanade.presentation.util.formattedMessage
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadCache 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.Chapter
import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.chapter.model.NoChaptersException import tachiyomi.domain.chapter.model.NoChaptersException
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
@ -99,6 +102,7 @@ class MangaScreenModel(
private val getTracks: GetTracks = Injekt.get(), private val getTracks: GetTracks = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(), private val mangaRepository: MangaRepository = Injekt.get(),
private val chapterRepository: ChapterRepository = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<MangaScreenModel.State>(State.Loading) { ) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
@ -196,6 +200,7 @@ class MangaScreenModel(
val fetchFromSourceTasks = listOf( val fetchFromSourceTasks = listOf(
async { if (needRefreshInfo) fetchMangaFromSource() }, async { if (needRefreshInfo) fetchMangaFromSource() },
async { if (needRefreshChapter) fetchChaptersFromSource() }, async { if (needRefreshChapter) fetchChaptersFromSource() },
async { if (needRefreshChapter) checkUserChaptersFromDeletion() },
) )
fetchFromSourceTasks.awaitAll() fetchFromSourceTasks.awaitAll()
} }
@ -211,12 +216,24 @@ class MangaScreenModel(
val fetchFromSourceTasks = listOf( val fetchFromSourceTasks = listOf(
async { fetchMangaFromSource(manualFetch) }, async { fetchMangaFromSource(manualFetch) },
async { fetchChaptersFromSource(manualFetch) }, async { fetchChaptersFromSource(manualFetch) },
async { checkUserChaptersFromDeletion() },
) )
fetchFromSourceTasks.awaitAll() fetchFromSourceTasks.awaitAll()
updateSuccessState { it.copy(isRefreshingData = false) } 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 // Manga info - start
/** /**
@ -692,6 +709,24 @@ class MangaScreenModel(
if (pointerPos != -1) markChaptersRead(prevChapters.take(pointerPos), true) if (pointerPos != -1) markChaptersRead(prevChapters.take(pointerPos), true)
} }
fun runLocalChapterActions(
items: List<ChapterItem>,
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. * Mark the selected chapter list as read/unread.
* @param chapters the list of selected chapters. * @param chapters the list of selected chapters.
@ -746,6 +781,8 @@ class MangaScreenModel(
state.source, state.source,
) )
} }
val toDeleteUserChaptersIds = chapters.filter { it.localChapter }.map { it.id }
chapterRepository.removeChaptersWithIds(toDeleteUserChaptersIds)
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
} }
@ -965,7 +1002,7 @@ class MangaScreenModel(
sealed interface Dialog { sealed interface Dialog {
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
data class DeleteChapters(val chapters: List<Chapter>) : Dialog data class DeleteChapters(val chapters: List<Chapter>, val includeUserChapter: Boolean) : Dialog
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
data class SetFetchInterval(val manga: Manga) : Dialog data class SetFetchInterval(val manga: Manga) : Dialog
data object SettingsSheet : Dialog data object SettingsSheet : Dialog
@ -978,7 +1015,7 @@ class MangaScreenModel(
} }
fun showDeleteChapterDialog(chapters: List<Chapter>) { fun showDeleteChapterDialog(chapters: List<Chapter>) {
updateSuccessState { it.copy(dialog = Dialog.DeleteChapters(chapters)) } updateSuccessState { it.copy(dialog = Dialog.DeleteChapters(chapters, chapters.fastAny { chapter -> chapter.localChapter })) }
} }
fun showSettingsDialog() { fun showSettingsDialog() {

View file

@ -468,7 +468,7 @@ class ReaderViewModel(
// Determine which chapter should be deleted and enqueue // Determine which chapter should be deleted and enqueue
val currentChapterPosition = chapterList.indexOf(currentChapter) 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 // If chapter is completely read, no need to download it
chapterToDownload = null chapterToDownload = null

View file

@ -5,6 +5,7 @@ import com.github.junrar.exception.UnsupportedRarV5Exception
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -57,6 +58,11 @@ class ChapterLoader(
chapter.state = ReaderChapter.State.Loaded(pages) chapter.state = ReaderChapter.State.Loaded(pages)
} catch (e: Throwable) { } 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) chapter.state = ReaderChapter.State.Error(e)
throw e throw e
} }

View file

@ -2,8 +2,8 @@ package tachiyomi.data.chapter
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Double, Long, Long, Long, Long) -> Chapter = 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 -> { id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload, lastModifiedAt, localChapter ->
Chapter( Chapter(
id = id, id = id,
mangaId = mangaId, mangaId = mangaId,
@ -18,5 +18,6 @@ val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long,
chapterNumber = chapterNumber, chapterNumber = chapterNumber,
scanlator = scanlator, scanlator = scanlator,
lastModifiedAt = lastModifiedAt, lastModifiedAt = lastModifiedAt,
localChapter = localChapter,
) )
} }

View file

@ -28,6 +28,7 @@ class ChapterRepositoryImpl(
chapter.sourceOrder, chapter.sourceOrder,
chapter.dateFetch, chapter.dateFetch,
chapter.dateUpload, chapter.dateUpload,
chapter.localChapter,
) )
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne() val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
chapter.copy(id = lastInsertId) chapter.copy(id = lastInsertId)
@ -63,6 +64,7 @@ class ChapterRepositoryImpl(
dateFetch = chapterUpdate.dateFetch, dateFetch = chapterUpdate.dateFetch,
dateUpload = chapterUpdate.dateUpload, dateUpload = chapterUpdate.dateUpload,
chapterId = chapterUpdate.id, chapterId = chapterUpdate.id,
localChapter = chapterUpdate.localChapter,
) )
} }
} }

View file

@ -14,6 +14,7 @@ CREATE TABLE chapters(
date_fetch INTEGER NOT NULL, date_fetch INTEGER NOT NULL,
date_upload INTEGER NOT NULL, date_upload INTEGER NOT NULL,
last_modified_at INTEGER NOT NULL DEFAULT 0, last_modified_at INTEGER NOT NULL DEFAULT 0,
local_chapter INTEGER AS Boolean NOT NULL DEFAULT 0,
FOREIGN KEY(manga_id) REFERENCES mangas (_id) FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE ON DELETE CASCADE
); );
@ -62,8 +63,8 @@ DELETE FROM chapters
WHERE _id IN :chapterIds; WHERE _id IN :chapterIds;
insert: 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) 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')); VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, strftime('%s', 'now'), :localChapter);
update: update:
UPDATE chapters UPDATE chapters
@ -77,7 +78,8 @@ SET manga_id = coalesce(:mangaId, manga_id),
chapter_number = coalesce(:chapterNumber, chapter_number), chapter_number = coalesce(:chapterNumber, chapter_number),
source_order = coalesce(:sourceOrder, source_order), source_order = coalesce(:sourceOrder, source_order),
date_fetch = coalesce(:dateFetch, date_fetch), 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; WHERE _id = :chapterId;
selectLastInsertedRowId: selectLastInsertedRowId:

View file

@ -0,0 +1,3 @@
ALTER TABLE chapters ADD COLUMN local_chapter INTEGER AS Boolean NOT NULL DEFAULT 0;
UPDATE chapters SET local_chapter = 0;

View file

@ -14,6 +14,7 @@ data class Chapter(
val chapterNumber: Double, val chapterNumber: Double,
val scanlator: String?, val scanlator: String?,
val lastModifiedAt: Long, val lastModifiedAt: Long,
val localChapter: Boolean,
) { ) {
val isRecognizedNumber: Boolean val isRecognizedNumber: Boolean
get() = chapterNumber >= 0f get() = chapterNumber >= 0f
@ -33,6 +34,7 @@ data class Chapter(
chapterNumber = -1.0, chapterNumber = -1.0,
scanlator = null, scanlator = null,
lastModifiedAt = 0, lastModifiedAt = 0,
localChapter = false,
) )
} }
} }

View file

@ -13,8 +13,9 @@ data class ChapterUpdate(
val dateUpload: Long? = null, val dateUpload: Long? = null,
val chapterNumber: Double? = null, val chapterNumber: Double? = null,
val scanlator: String? = null, val scanlator: String? = null,
val localChapter: Boolean? = null,
) )
fun Chapter.toChapterUpdate(): ChapterUpdate { 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)
} }

View file

@ -688,6 +688,7 @@
<string name="error_saving_cover">Error saving cover</string> <string name="error_saving_cover">Error saving cover</string>
<string name="error_sharing_cover">Error sharing cover</string> <string name="error_sharing_cover">Error sharing cover</string>
<string name="confirm_delete_chapters">Are you sure you want to delete the selected chapters?</string> <string name="confirm_delete_chapters">Are you sure you want to delete the selected chapters?</string>
<string name="confirm_delete_user_chapters">Are you sure you want to delete the selected chapters? They include local files that may be irretrievably lost after deletion.</string>
<string name="chapter_settings">Chapter settings</string> <string name="chapter_settings">Chapter settings</string>
<string name="confirm_set_chapter_settings">Are you sure you want to save these settings as default?</string> <string name="confirm_set_chapter_settings">Are you sure you want to save these settings as default?</string>
<string name="also_set_chapter_settings_for_library">Also apply to all entries in my library</string> <string name="also_set_chapter_settings_for_library">Also apply to all entries in my library</string>
@ -775,6 +776,7 @@
<string name="transition_pages_loading">Loading pages…</string> <string name="transition_pages_loading">Loading pages…</string>
<string name="transition_pages_error">Failed to load pages: %1$s</string> <string name="transition_pages_error">Failed to load pages: %1$s</string>
<string name="page_list_empty_error">No pages found</string> <string name="page_list_empty_error">No pages found</string>
<string name="local_chapter_not_found">Local chapter file not found</string>
<string name="loader_not_implemented_error">Source not found</string> <string name="loader_not_implemented_error">Source not found</string>
<string name="loader_rar5_error">RARv5 format is not supported</string> <string name="loader_rar5_error">RARv5 format is not supported</string>
<plurals name="missing_chapters_warning"> <plurals name="missing_chapters_warning">