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()) {
chaptersToUpdate
.filterNot { it.localChapter }
.groupBy { it.mangaId }
.forEach { (mangaId, chapters) ->
deleteDownload.awaitAll(

View file

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

View file

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

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.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
},
)
}
}

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)

View file

@ -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) {

View file

@ -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(

View file

@ -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() {

View file

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

View file

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

View file

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

View file

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

View file

@ -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:

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

View file

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

View file

@ -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">