Add localChapter
This commit is contained in:
parent
81cd765543
commit
50faf3d437
24 changed files with 221 additions and 13 deletions
|
@ -48,6 +48,7 @@ class SetReadStatus(
|
|||
|
||||
if (read && downloadPreferences.removeAfterMarkedAsRead().get()) {
|
||||
chaptersToUpdate
|
||||
.filterNot { it.localChapter }
|
||||
.groupBy { it.mangaId }
|
||||
.forEach { (mangaId, chapters) ->
|
||||
deleteDownload.awaitAll(
|
||||
|
|
|
@ -80,7 +80,7 @@ class SyncChaptersWithSource(
|
|||
val toDelete = dbChapters.filterNot { dbChapter ->
|
||||
sourceChapters.any { sourceChapter ->
|
||||
dbChapter.url == sourceChapter.url
|
||||
}
|
||||
} || dbChapter.localChapter
|
||||
}
|
||||
|
||||
val rightNow = Date().time
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<ChapterItem>, 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<ChapterItem>, 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<ChapterItem>, 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<ChapterItem>, ChapterDownloadAction) -> Unit)?,
|
||||
onChapterSelected: (ChapterItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSwipe: (ChapterItem, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
onLocalChapter: ((List<ChapterItem>, 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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<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> {
|
||||
// Retrieve the categories that are set to exclude from being deleted on read
|
||||
val categoriesToExclude = downloadPreferences.removeExcludeCategories().get().map(String::toLong)
|
||||
|
|
|
@ -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<MigrateDialogScreenModel.State>(State()) {
|
||||
|
||||
val migrateFlags: Preference<Int> 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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<MangaScreenModel.State>(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<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.
|
||||
* @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<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 SetFetchInterval(val manga: Manga) : Dialog
|
||||
data object SettingsSheet : Dialog
|
||||
|
@ -978,7 +1015,7 @@ class MangaScreenModel(
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
3
data/src/main/sqldelight/tachiyomi/migrations/26.sqm
Normal file
3
data/src/main/sqldelight/tachiyomi/migrations/26.sqm
Normal 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;
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -688,6 +688,7 @@
|
|||
<string name="error_saving_cover">Error saving 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_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="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>
|
||||
|
@ -775,6 +776,7 @@
|
|||
<string name="transition_pages_loading">Loading pages…</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="local_chapter_not_found">Local chapter file not found</string>
|
||||
<string name="loader_not_implemented_error">Source not found</string>
|
||||
<string name="loader_rar5_error">RARv5 format is not supported</string>
|
||||
<plurals name="missing_chapters_warning">
|
||||
|
|
Reference in a new issue