Add per-page bookmark with optional notes and new view.
This commit is contained in:
parent
22589a9c30
commit
89859f1e52
43 changed files with 1972 additions and 33 deletions
|
@ -22,7 +22,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
|
|
||||||
versionCode = 113
|
versionCode = 114
|
||||||
versionName = "0.14.7"
|
versionName = "0.14.7"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
|
|
|
@ -22,6 +22,7 @@ import eu.kanade.domain.track.interactor.AddTracks
|
||||||
import eu.kanade.domain.track.interactor.RefreshTracks
|
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||||
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||||
import eu.kanade.domain.track.interactor.TrackChapter
|
import eu.kanade.domain.track.interactor.TrackChapter
|
||||||
|
import tachiyomi.data.bookmark.BookmarkRepositoryImpl
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||||
|
@ -31,6 +32,13 @@ import tachiyomi.data.source.SourceRepositoryImpl
|
||||||
import tachiyomi.data.source.StubSourceRepositoryImpl
|
import tachiyomi.data.source.StubSourceRepositoryImpl
|
||||||
import tachiyomi.data.track.TrackRepositoryImpl
|
import tachiyomi.data.track.TrackRepositoryImpl
|
||||||
import tachiyomi.data.updates.UpdatesRepositoryImpl
|
import tachiyomi.data.updates.UpdatesRepositoryImpl
|
||||||
|
import tachiyomi.domain.bookmark.interactor.DeleteBookmark
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmark
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmarkedMangas
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmarkedPages
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmarks
|
||||||
|
import tachiyomi.domain.bookmark.interactor.SetBookmark
|
||||||
|
import tachiyomi.domain.bookmark.repository.BookmarkRepository
|
||||||
import tachiyomi.domain.category.interactor.CreateCategoryWithName
|
import tachiyomi.domain.category.interactor.CreateCategoryWithName
|
||||||
import tachiyomi.domain.category.interactor.DeleteCategory
|
import tachiyomi.domain.category.interactor.DeleteCategory
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
@ -138,7 +146,7 @@ class DomainModule : InjektModule {
|
||||||
addFactory { UpdateChapter(get()) }
|
addFactory { UpdateChapter(get()) }
|
||||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||||
addFactory { ShouldUpdateDbChapter() }
|
addFactory { ShouldUpdateDbChapter() }
|
||||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
|
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||||
addFactory { GetAvailableScanlators(get()) }
|
addFactory { GetAvailableScanlators(get()) }
|
||||||
|
|
||||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||||
|
@ -167,5 +175,13 @@ class DomainModule : InjektModule {
|
||||||
addFactory { ToggleLanguage(get()) }
|
addFactory { ToggleLanguage(get()) }
|
||||||
addFactory { ToggleSource(get()) }
|
addFactory { ToggleSource(get()) }
|
||||||
addFactory { ToggleSourcePin(get()) }
|
addFactory { ToggleSourcePin(get()) }
|
||||||
|
|
||||||
|
addSingletonFactory<BookmarkRepository> { BookmarkRepositoryImpl(get()) }
|
||||||
|
addFactory { SetBookmark(get(), get()) }
|
||||||
|
addFactory { DeleteBookmark(get(), get()) }
|
||||||
|
addFactory { GetBookmark(get()) }
|
||||||
|
addFactory { GetBookmarks(get()) }
|
||||||
|
addFactory { GetBookmarkedMangas(get()) }
|
||||||
|
addFactory { GetBookmarkedPages(get()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@ import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import tachiyomi.data.chapter.ChapterSanitizer
|
import tachiyomi.data.chapter.ChapterSanitizer
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmarks
|
||||||
|
import tachiyomi.domain.bookmark.interactor.SetBookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkWithChapterNumber
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
|
@ -34,6 +37,8 @@ class SyncChaptersWithSource(
|
||||||
private val updateChapter: UpdateChapter,
|
private val updateChapter: UpdateChapter,
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
private val getExcludedScanlators: GetExcludedScanlators,
|
private val getExcludedScanlators: GetExcludedScanlators,
|
||||||
|
private val getBookmarks: GetBookmarks,
|
||||||
|
private val setBookmark: SetBookmark,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,6 +148,12 @@ class SyncChaptersWithSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
val reAdded = mutableListOf<Chapter>()
|
val reAdded = mutableListOf<Chapter>()
|
||||||
|
val reAddedBookmarks = mutableListOf<BookmarkWithChapterNumber>()
|
||||||
|
val bookmarksByChapterNumber = if (newChapters.isEmpty()) {
|
||||||
|
emptyMap()
|
||||||
|
} else {
|
||||||
|
getBookmarks.awaitWithChapterNumbers(manga.id).groupBy { it.chapterNumber }
|
||||||
|
}
|
||||||
|
|
||||||
val deletedChapterNumbers = TreeSet<Double>()
|
val deletedChapterNumbers = TreeSet<Double>()
|
||||||
val deletedReadChapterNumbers = TreeSet<Double>()
|
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||||
|
@ -170,6 +181,9 @@ class SyncChaptersWithSource(
|
||||||
bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers,
|
bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Existing bookmarks are saved to be moved to re-added chapters.
|
||||||
|
bookmarksByChapterNumber[chapter.chapterNumber]?.let { reAddedBookmarks.addAll(it) }
|
||||||
|
|
||||||
// Try to to use the fetch date of the original entry to not pollute 'Updates' tab
|
// Try to to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||||
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||||
chapter = chapter.copy(dateFetch = it)
|
chapter = chapter.copy(dateFetch = it)
|
||||||
|
@ -193,6 +207,22 @@ class SyncChaptersWithSource(
|
||||||
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reAddedBookmarks.isNotEmpty()) {
|
||||||
|
val chapterIdByNumber = updatedToAdd.associate { it.chapterNumber to it.id }
|
||||||
|
val bookmarksToAdd = reAddedBookmarks.mapNotNull { bm ->
|
||||||
|
chapterIdByNumber[bm.chapterNumber]
|
||||||
|
?.let { chapterId ->
|
||||||
|
bm.toBookmarkImpl().copy(
|
||||||
|
mangaId = manga.id,
|
||||||
|
chapterId = chapterId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setBookmark.awaitAll(bookmarksToAdd, updateChapters = false)
|
||||||
|
}
|
||||||
|
|
||||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||||
|
|
||||||
// Set this manga as updated since chapters were changed
|
// Set this manga as updated since chapters were changed
|
||||||
|
|
|
@ -0,0 +1,225 @@
|
||||||
|
package eu.kanade.presentation.bookmarks
|
||||||
|
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEachIndexed
|
||||||
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.bookmarks.BookmarksTopScreenModel
|
||||||
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkedPage
|
||||||
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import tachiyomi.domain.manga.model.MangaCover as CoverData
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookmarksDetailsScreenContent(
|
||||||
|
state: BookmarksTopScreenModel.State,
|
||||||
|
paddingValues: PaddingValues,
|
||||||
|
relativeTime: Boolean,
|
||||||
|
dateFormat: DateFormat,
|
||||||
|
onBookmarkClick: (mangaId: Long, chapterId: Long, pageIndex: Int?) -> Unit,
|
||||||
|
onMangaClick: (mangaId: Long) -> Unit,
|
||||||
|
) {
|
||||||
|
val statListState = rememberLazyListState()
|
||||||
|
|
||||||
|
ScrollbarLazyColumn(
|
||||||
|
contentPadding = paddingValues + PaddingValues(vertical = MaterialTheme.padding.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
state = statListState,
|
||||||
|
) {
|
||||||
|
state.groupsOfBookmarks.fastForEachIndexed { i, bookmark ->
|
||||||
|
// Header:
|
||||||
|
if (i == 0 || bookmark.mangaId != state.groupsOfBookmarks[i - 1].mangaId) {
|
||||||
|
item(
|
||||||
|
key = "bm-header-${bookmark.mangaId}",
|
||||||
|
contentType = "header",
|
||||||
|
) {
|
||||||
|
MangaCoverUiItem(
|
||||||
|
bookmark.coverData,
|
||||||
|
bookmark.mangaTitle,
|
||||||
|
onMangaClick = { onMangaClick(bookmark.mangaId) },
|
||||||
|
Modifier.animateItemPlacement(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "bm-id-${bookmark.bookmarkId}") {
|
||||||
|
BookmarkUiItem(
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
info = bookmark,
|
||||||
|
relativeTime = relativeTime,
|
||||||
|
dateFormat = dateFormat,
|
||||||
|
onLongClick = {},
|
||||||
|
onClick = {
|
||||||
|
onBookmarkClick(
|
||||||
|
bookmark.mangaId,
|
||||||
|
bookmark.chapterId,
|
||||||
|
bookmark.pageIndex,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaCoverUiItem(
|
||||||
|
coverData: CoverData,
|
||||||
|
title: String,
|
||||||
|
onMangaClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.combinedClickable(onClick = onMangaClick)
|
||||||
|
.height(56.dp)
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||||
|
) {
|
||||||
|
MangaCover.Square(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 6.dp)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
data = coverData,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.weight(weight = 1f, fill = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookmarkUiItem(
|
||||||
|
info: BookmarkedPage,
|
||||||
|
relativeTime: Boolean,
|
||||||
|
dateFormat: DateFormat,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val maxLinesForNoteText = 5
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick()
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.padding(
|
||||||
|
vertical = MaterialTheme.padding.extraSmall,
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = info.chapterName,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = info.pageIndex?.let { i ->
|
||||||
|
stringResource(R.string.bookmark_page_number, i + 1)
|
||||||
|
} ?: stringResource(R.string.bookmark_chapter),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha),
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = Date(info.lastModifiedAt).toRelativeString(context, relativeTime, dateFormat),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
info.note?.takeIf { it.isNotBlank() }?.let { text ->
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
maxLines = maxLinesForNoteText,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun BookmarkUiItemPreview() {
|
||||||
|
BookmarkUiItem(
|
||||||
|
modifier = Modifier,
|
||||||
|
info = BookmarkedPage(
|
||||||
|
lastModifiedAt = 123123,
|
||||||
|
note = "Very long note here, ....sdf ljsadf kaslkdfjlkjlkdf , long ,asdkl jaskdjlkajsdlklkjlksdf lasfd ABC",
|
||||||
|
bookmarkId = 1,
|
||||||
|
pageIndex = 12,
|
||||||
|
chapterName = "Chapte sadfjhks dfjksad kfjhksjdhf kjhsdfkj hkfdhkajdfh r",
|
||||||
|
mangaId = 1,
|
||||||
|
chapterId = 12,
|
||||||
|
chapterNumber = 1.0,
|
||||||
|
coverData = CoverData(
|
||||||
|
mangaId = 1,
|
||||||
|
isMangaFavorite = true,
|
||||||
|
lastModified = 1,
|
||||||
|
sourceId = 1,
|
||||||
|
url = null,
|
||||||
|
),
|
||||||
|
mangaTitle = "Manga",
|
||||||
|
),
|
||||||
|
relativeTime = true,
|
||||||
|
dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()),
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
package eu.kanade.presentation.bookmarks
|
||||||
|
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
|
import eu.kanade.tachiyomi.ui.bookmarks.BookmarksTopScreenModel
|
||||||
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
|
import tachiyomi.domain.bookmark.model.MangaWithBookmarks
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookmarksTopScreenContent(
|
||||||
|
state: BookmarksTopScreenModel.State,
|
||||||
|
paddingValues: PaddingValues,
|
||||||
|
relativeTime: Boolean,
|
||||||
|
dateFormat: DateFormat,
|
||||||
|
onMangaSelected: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
val statListState = rememberLazyListState()
|
||||||
|
ScrollbarLazyColumn(
|
||||||
|
contentPadding = paddingValues + PaddingValues(vertical = MaterialTheme.padding.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||||
|
state = statListState,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = state.mangaWithBookmarks,
|
||||||
|
key = { "bm-manga-${it.mangaId}" },
|
||||||
|
) {
|
||||||
|
MangaWithBookmarksUiItem(
|
||||||
|
info = it,
|
||||||
|
relativeTime = relativeTime,
|
||||||
|
dateFormat = dateFormat,
|
||||||
|
onLongClick = {
|
||||||
|
onMangaSelected(it.mangaId)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onMangaSelected(it.mangaId)
|
||||||
|
},
|
||||||
|
onClickCover = {
|
||||||
|
onMangaSelected(it.mangaId)
|
||||||
|
},
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MangaWithBookmarksUiItem(
|
||||||
|
info: MangaWithBookmarks,
|
||||||
|
relativeTime: Boolean,
|
||||||
|
dateFormat: DateFormat,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
onClickCover: (() -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick()
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.height(56.dp)
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
MangaCover.Square(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 6.dp)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
data = info.coverData,
|
||||||
|
onClick = onClickCover,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium)
|
||||||
|
.weight(1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = info.mangaTitle,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
var textHeight by remember { mutableIntStateOf(0) }
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Bookmark,
|
||||||
|
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
|
||||||
|
modifier = Modifier
|
||||||
|
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
MR.strings.bookmark_total_in_manga,
|
||||||
|
info.numberOfBookmarks,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
onTextLayout = { textHeight = it.size.height },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(weight = 1f, fill = false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.bookmarkLastModified > 0) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
MR.strings.bookmark_last_updated_in_manga,
|
||||||
|
Date(info.bookmarkLastModified).toRelativeString(context, relativeTime, dateFormat),
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
|
import androidx.compose.material.icons.outlined.Bookmarks
|
||||||
import androidx.compose.material.icons.outlined.CloudOff
|
import androidx.compose.material.icons.outlined.CloudOff
|
||||||
import androidx.compose.material.icons.outlined.GetApp
|
import androidx.compose.material.icons.outlined.GetApp
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
@ -46,6 +47,7 @@ fun MoreScreen(
|
||||||
onClickDownloadQueue: () -> Unit,
|
onClickDownloadQueue: () -> Unit,
|
||||||
onClickCategories: () -> Unit,
|
onClickCategories: () -> Unit,
|
||||||
onClickStats: () -> Unit,
|
onClickStats: () -> Unit,
|
||||||
|
onClickBookmarks: () -> Unit,
|
||||||
onClickDataAndStorage: () -> Unit,
|
onClickDataAndStorage: () -> Unit,
|
||||||
onClickSettings: () -> Unit,
|
onClickSettings: () -> Unit,
|
||||||
onClickAbout: () -> Unit,
|
onClickAbout: () -> Unit,
|
||||||
|
@ -142,6 +144,13 @@ fun MoreScreen(
|
||||||
onPreferenceClick = onClickStats,
|
onPreferenceClick = onClickStats,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(MR.strings.label_bookmarks),
|
||||||
|
icon = Icons.Outlined.Bookmarks,
|
||||||
|
onPreferenceClick = onClickBookmarks,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(MR.strings.label_data_storage),
|
title = stringResource(MR.strings.label_data_storage),
|
||||||
|
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Bookmark
|
||||||
import androidx.compose.material.icons.outlined.Photo
|
import androidx.compose.material.icons.outlined.Photo
|
||||||
import androidx.compose.material.icons.outlined.Save
|
import androidx.compose.material.icons.outlined.Save
|
||||||
import androidx.compose.material.icons.outlined.Share
|
import androidx.compose.material.icons.outlined.Share
|
||||||
|
@ -19,6 +20,8 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
|
import eu.kanade.tachiyomi.ui.bookmarks.EditBookmarkDialog
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ActionButton
|
import tachiyomi.presentation.core.components.ActionButton
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
@ -30,8 +33,12 @@ fun ReaderPageActionsDialog(
|
||||||
onSetAsCover: () -> Unit,
|
onSetAsCover: () -> Unit,
|
||||||
onShare: () -> Unit,
|
onShare: () -> Unit,
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
|
onBookmarkPage: (String) -> Unit,
|
||||||
|
onUnbookmarkPage: () -> Unit,
|
||||||
|
getPageBookmark: () -> Bookmark?,
|
||||||
) {
|
) {
|
||||||
var showSetCoverDialog by remember { mutableStateOf(false) }
|
var showSetCoverDialog by remember { mutableStateOf(false) }
|
||||||
|
var showEditPageBookmarkDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -64,6 +71,12 @@ fun ReaderPageActionsDialog(
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
ActionButton(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
title = stringResource(MR.strings.page_bookmark),
|
||||||
|
icon = Icons.Outlined.Bookmark,
|
||||||
|
onClick = { showEditPageBookmarkDialog = true },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +89,23 @@ fun ReaderPageActionsDialog(
|
||||||
onDismiss = { showSetCoverDialog = false },
|
onDismiss = { showSetCoverDialog = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showEditPageBookmarkDialog) {
|
||||||
|
EditBookmarkDialog(
|
||||||
|
onConfirm = { bookmarkNote ->
|
||||||
|
onBookmarkPage(bookmarkNote)
|
||||||
|
showEditPageBookmarkDialog = false
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
onDelete = {
|
||||||
|
onUnbookmarkPage()
|
||||||
|
showEditPageBookmarkDialog = false
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
onDismiss = { showEditPageBookmarkDialog = false },
|
||||||
|
bookmark = getPageBookmark(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -10,6 +10,7 @@ data class BackupOptions(
|
||||||
val chapters: Boolean = true,
|
val chapters: Boolean = true,
|
||||||
val tracking: Boolean = true,
|
val tracking: Boolean = true,
|
||||||
val history: Boolean = true,
|
val history: Boolean = true,
|
||||||
|
val bookmarks: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
|
@ -24,6 +25,7 @@ data class BackupOptions(
|
||||||
appSettings,
|
appSettings,
|
||||||
sourceSettings,
|
sourceSettings,
|
||||||
privateSettings,
|
privateSettings,
|
||||||
|
bookmarks,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun anyEnabled() = libraryEntries || appSettings || sourceSettings
|
fun anyEnabled() = libraryEntries || appSettings || sourceSettings
|
||||||
|
@ -59,6 +61,12 @@ data class BackupOptions(
|
||||||
setter = { options, enabled -> options.copy(history = enabled) },
|
setter = { options, enabled -> options.copy(history = enabled) },
|
||||||
enabled = { it.libraryEntries },
|
enabled = { it.libraryEntries },
|
||||||
),
|
),
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.label_bookmarks,
|
||||||
|
getter = BackupOptions::bookmarks,
|
||||||
|
setter = { options, enabled -> options.copy(bookmarks = enabled) },
|
||||||
|
enabled = { it.libraryEntries },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val settingsOptions = persistentListOf(
|
val settingsOptions = persistentListOf(
|
||||||
|
@ -89,6 +97,7 @@ data class BackupOptions(
|
||||||
appSettings = array[5],
|
appSettings = array[5],
|
||||||
sourceSettings = array[6],
|
sourceSettings = array[6],
|
||||||
privateSettings = array[7],
|
privateSettings = array[7],
|
||||||
|
bookmarks = array.getOrElse(8) { true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupBookmarkMapper
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
|
@ -71,6 +72,15 @@ class MangaBackupCreator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.bookmarks) {
|
||||||
|
val bookmarks = handler.awaitList {
|
||||||
|
bookmarksQueries.getWithChapterInfoByMangaId(manga.id, backupBookmarkMapper)
|
||||||
|
}
|
||||||
|
if (bookmarks.isNotEmpty()) {
|
||||||
|
mangaObject.bookmarks = bookmarks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return mangaObject
|
return mangaObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BackupBookmark(
|
||||||
|
@ProtoNumber(1) var chapterUrl: String,
|
||||||
|
@ProtoNumber(2) var pageIndex: Int? = null,
|
||||||
|
@ProtoNumber(3) var note: String? = null,
|
||||||
|
@ProtoNumber(4) var lastModifiedAt: Long = 0,
|
||||||
|
) {
|
||||||
|
fun toBookmarkImpl(): Bookmark {
|
||||||
|
return Bookmark.create()
|
||||||
|
.copy(
|
||||||
|
pageIndex = pageIndex,
|
||||||
|
note = note,
|
||||||
|
lastModifiedAt = lastModifiedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupBookmarkMapper =
|
||||||
|
{ chapterUrl: String, _: Double, pageIndex: Long?, note: String?, lastModifiedAt: Long ->
|
||||||
|
BackupBookmark(
|
||||||
|
chapterUrl = chapterUrl,
|
||||||
|
pageIndex = pageIndex?.toInt(),
|
||||||
|
note = note,
|
||||||
|
lastModifiedAt = lastModifiedAt * 1000L,
|
||||||
|
)
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ data class BackupManga(
|
||||||
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||||
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
||||||
|
@ProtoNumber(108) var bookmarks: List<BackupBookmark> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun getMangaImpl(): Manga {
|
fun getMangaImpl(): Manga {
|
||||||
return Manga.create().copy(
|
return Manga.create().copy(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||||
|
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupBookmark
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
|
@ -8,6 +9,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
|
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||||
|
import tachiyomi.domain.bookmark.interactor.SetBookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
@ -31,6 +34,7 @@ class MangaRestorer(
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
private val getTracks: GetTracks = Injekt.get(),
|
private val getTracks: GetTracks = Injekt.get(),
|
||||||
private val insertTrack: InsertTrack = Injekt.get(),
|
private val insertTrack: InsertTrack = Injekt.get(),
|
||||||
|
private val setBookmark: SetBookmark = Injekt.get(),
|
||||||
fetchInterval: FetchInterval = Injekt.get(),
|
fetchInterval: FetchInterval = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -72,6 +76,7 @@ class MangaRestorer(
|
||||||
backupCategories = backupCategories,
|
backupCategories = backupCategories,
|
||||||
history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
|
history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
|
||||||
tracks = backupManga.tracking,
|
tracks = backupManga.tracking,
|
||||||
|
bookmarks = backupManga.bookmarks,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,11 +267,13 @@ class MangaRestorer(
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<BackupTracking>,
|
tracks: List<BackupTracking>,
|
||||||
|
bookmarks: List<BackupBookmark>,
|
||||||
): Manga {
|
): Manga {
|
||||||
restoreCategories(manga, categories, backupCategories)
|
restoreCategories(manga, categories, backupCategories)
|
||||||
restoreChapters(manga, chapters)
|
restoreChapters(manga, chapters)
|
||||||
restoreTracking(manga, tracks)
|
restoreTracking(manga, tracks)
|
||||||
restoreHistory(history)
|
restoreHistory(history)
|
||||||
|
restoreBookmarks(manga, bookmarks)
|
||||||
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
|
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
@ -398,5 +405,36 @@ class MangaRestorer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreBookmarks(manga: Manga, backupBookmarks: List<BackupBookmark>) {
|
||||||
|
val chapters = getChaptersByMangaId.await(manga.id)
|
||||||
|
|
||||||
|
val bookmarks: List<Bookmark> =
|
||||||
|
if (backupBookmarks.isEmpty()) {
|
||||||
|
// No bookmarks list in the backup.
|
||||||
|
// It's either an older version backup or backup without bookmarks.
|
||||||
|
// Create chapter-level bookmarks (they will not affect existing chapter bookmarks).
|
||||||
|
chapters
|
||||||
|
.filter { it.bookmark }
|
||||||
|
.map { Bookmark.create().copy(mangaId = manga.id, chapterId = it.id) }
|
||||||
|
} else {
|
||||||
|
// Map chapters from backup to db chapters and insert bookmark records based on backup.
|
||||||
|
val chapterIdByUrl = chapters.associate { it.url to it.id }
|
||||||
|
backupBookmarks.mapNotNull {
|
||||||
|
chapterIdByUrl[it.chapterUrl]
|
||||||
|
?.let { chapterId ->
|
||||||
|
it.toBookmarkImpl()
|
||||||
|
.copy(
|
||||||
|
mangaId = manga.id,
|
||||||
|
chapterId = chapterId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookmarks.isNotEmpty()) {
|
||||||
|
setBookmark.awaitAll(bookmarks, updateChapters = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L)
|
private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.bookmarks
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.DeleteSweep
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.bookmarks.BookmarksDetailsScreenContent
|
||||||
|
import eu.kanade.presentation.bookmarks.BookmarksTopScreenContent
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-level screen for bookmarks.
|
||||||
|
* Displays aggregated information by manga with details on manga selection.
|
||||||
|
*/
|
||||||
|
class BookmarksTopScreen : Screen() {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val screenModel = rememberScreenModel { BookmarksTopScreenModel() }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
var showRemoveAllConfirmationDialog by remember { mutableStateOf(false) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
AppBar(
|
||||||
|
title = stringResource(MR.strings.label_bookmarks),
|
||||||
|
navigateUp = { screenModel.onNavigationUp(navigator) },
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
actions = {
|
||||||
|
state.selectedMangaId?.let {
|
||||||
|
AppBarActions(
|
||||||
|
persistentListOf(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(MR.strings.action_delete_all_bookmarks),
|
||||||
|
icon = Icons.Outlined.DeleteSweep,
|
||||||
|
onClick = {
|
||||||
|
showRemoveAllConfirmationDialog = true
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
when {
|
||||||
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
|
|
||||||
|
state.isEmpty ->
|
||||||
|
EmptyScreen(
|
||||||
|
stringRes = MR.strings.information_no_bookmarks,
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
PullRefresh(
|
||||||
|
refreshing = state.isRefreshing,
|
||||||
|
onRefresh = screenModel::refresh,
|
||||||
|
indicatorPadding = contentPadding,
|
||||||
|
enabled = { true },
|
||||||
|
) {
|
||||||
|
when (state.selectedMangaId) {
|
||||||
|
null ->
|
||||||
|
BookmarksTopScreenContent(
|
||||||
|
paddingValues = contentPadding,
|
||||||
|
state = state,
|
||||||
|
relativeTime = screenModel.relativeTime,
|
||||||
|
dateFormat = screenModel.dateFormat,
|
||||||
|
onMangaSelected = screenModel::onMangaSelected,
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> BookmarksDetailsScreenContent(
|
||||||
|
paddingValues = contentPadding,
|
||||||
|
state = state,
|
||||||
|
relativeTime = screenModel.relativeTime,
|
||||||
|
dateFormat = screenModel.dateFormat,
|
||||||
|
onBookmarkClick = { mangaId, chapterId, pageIndex ->
|
||||||
|
screenModel.openReader(
|
||||||
|
context,
|
||||||
|
mangaId,
|
||||||
|
chapterId,
|
||||||
|
pageIndex,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onMangaClick = { mangaId ->
|
||||||
|
navigator.push(MangaScreen(mangaId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showRemoveAllConfirmationDialog) {
|
||||||
|
BookmarksDeleteAllDialog(
|
||||||
|
onDelete = {
|
||||||
|
screenModel.delete()
|
||||||
|
showRemoveAllConfirmationDialog = false
|
||||||
|
},
|
||||||
|
onDismissRequest = { showRemoveAllConfirmationDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookmarksDeleteAllDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.action_delete_all_bookmarks))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(MR.strings.bookmark_delete_manga_confirmation))
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDelete) {
|
||||||
|
Text(text = stringResource(MR.strings.action_delete))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.bookmarks
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
|
import eu.kanade.core.preference.asState
|
||||||
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.core.util.lang.launchIO
|
||||||
|
import tachiyomi.domain.bookmark.interactor.DeleteBookmark
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmarkedMangas
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmarkedPages
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkedPage
|
||||||
|
import tachiyomi.domain.bookmark.model.MangaWithBookmarks
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class BookmarksTopScreenModel(
|
||||||
|
private val getBookmarkedMangas: GetBookmarkedMangas = Injekt.get(),
|
||||||
|
private val getBookmarkedPages: GetBookmarkedPages = Injekt.get(),
|
||||||
|
private val deleteBookmark: DeleteBookmark = Injekt.get(),
|
||||||
|
uiPreferences: UiPreferences = Injekt.get(),
|
||||||
|
) : StateScreenModel<BookmarksTopScreenModel.State>(State()) {
|
||||||
|
val relativeTime by uiPreferences.relativeTime().asState(screenModelScope)
|
||||||
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
|
|
||||||
|
init {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
loadMangaWithBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
mutableState.update { it.copy(isRefreshing = true) }
|
||||||
|
delay(0.5.seconds)
|
||||||
|
|
||||||
|
state.value.selectedMangaId?.let { mangaId ->
|
||||||
|
loadGroupedBookmarks(mangaId)
|
||||||
|
} ?: loadMangaWithBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openReader(context: Context, mangaId: Long, chapterId: Long, pageIndex: Int?) {
|
||||||
|
context.startActivity(ReaderActivity.newIntent(context, mangaId, chapterId, pageIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMangaSelected(mangaId: Long) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
loadGroupedBookmarks(mangaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNavigationUp(navigator: Navigator) {
|
||||||
|
if (state.value.selectedMangaId != null) {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
selectedMangaId = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigator.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete() {
|
||||||
|
state.value.selectedMangaId?.let { mangaId ->
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
deleteBookmark.awaitAllByMangaId(mangaId)
|
||||||
|
// Refresh after deletion and return to top level view.
|
||||||
|
loadMangaWithBookmarks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadMangaWithBookmarks() {
|
||||||
|
val mangaWithBookmarks = getBookmarkedMangas.await()
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
mangaWithBookmarks = mangaWithBookmarks,
|
||||||
|
groupsOfBookmarks = listOf(),
|
||||||
|
isLoading = false,
|
||||||
|
isRefreshing = false,
|
||||||
|
selectedMangaId = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadGroupedBookmarks(mangaId: Long) {
|
||||||
|
val bookmarks = getBookmarkedPages.await(mangaId)
|
||||||
|
val groupsOfBookmarks = bookmarks
|
||||||
|
.sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.mangaId },
|
||||||
|
{ it.chapterNumber },
|
||||||
|
// chapterName is needed for sorting when all numbers are -1 (e.g. Volume 1, Volume 2)
|
||||||
|
{ it.chapterName },
|
||||||
|
{ it.pageIndex ?: -1 },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
groupsOfBookmarks = groupsOfBookmarks,
|
||||||
|
isLoading = false,
|
||||||
|
isRefreshing = false,
|
||||||
|
selectedMangaId = mangaId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class State(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val isRefreshing: Boolean = false,
|
||||||
|
val mangaWithBookmarks: List<MangaWithBookmarks> = listOf(),
|
||||||
|
val groupsOfBookmarks: List<BookmarkedPage> = listOf(),
|
||||||
|
val selectedMangaId: Long? = null,
|
||||||
|
) {
|
||||||
|
val isEmpty =
|
||||||
|
selectedMangaId?.let { groupsOfBookmarks.isEmpty() } ?: mangaWithBookmarks.isEmpty()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.bookmarks
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for creating a new Bookmark, for updating or removing an existing Bookmark.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun EditBookmarkDialog(
|
||||||
|
onConfirm: (note: String) -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
bookmark: Bookmark?,
|
||||||
|
) {
|
||||||
|
var bookmarkNoteText by remember { mutableStateOf(bookmark?.note ?: "") }
|
||||||
|
// For in-place delete confirmation.
|
||||||
|
var showDeleteConfirmation by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val saveButtonText = bookmark?.let { stringResource(MR.strings.action_update_bookmark) }
|
||||||
|
?: stringResource(MR.strings.action_add)
|
||||||
|
|
||||||
|
val titleText = bookmark?.let { stringResource(MR.strings.action_update_page_bookmark) }
|
||||||
|
?: stringResource(MR.strings.action_add_page_bookmark)
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = if (showDeleteConfirmation) {
|
||||||
|
stringResource(MR.strings.action_delete_bookmark)
|
||||||
|
} else {
|
||||||
|
titleText
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
if (showDeleteConfirmation) {
|
||||||
|
Text(text = stringResource(MR.strings.delete_bookmark_confirmation))
|
||||||
|
} else {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
value = bookmarkNoteText,
|
||||||
|
onValueChange = { bookmarkNoteText = it },
|
||||||
|
label = { Text(stringResource(MR.strings.bookmark_note_placeholder)) },
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = 10,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Row {
|
||||||
|
if (showDeleteConfirmation) {
|
||||||
|
TextButton(onClick = { showDeleteConfirmation = false }) {
|
||||||
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
|
}
|
||||||
|
TextButton(onClick = { onDelete() }) {
|
||||||
|
Text(text = stringResource(MR.strings.action_delete))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (bookmark != null) {
|
||||||
|
TextButton(onClick = { showDeleteConfirmation = true }) {
|
||||||
|
Text(text = stringResource(MR.strings.action_delete))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextButton(onClick = { onConfirm(bookmarkNoteText) }) {
|
||||||
|
Text(text = saveButtonText)
|
||||||
|
}
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(MR.strings.action_cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(focusRequester) {
|
||||||
|
// TODO: https://issuetracker.google.com/issues/204502668
|
||||||
|
delay(0.1.seconds)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EditPageBookmarkDialogPreview() {
|
||||||
|
EditBookmarkDialog(
|
||||||
|
onConfirm = { },
|
||||||
|
onDelete = {},
|
||||||
|
onDismiss = {},
|
||||||
|
bookmark = Bookmark(1, 1, 1, 10, "ABC", 2),
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmarks
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
@ -30,9 +32,11 @@ object MigrationFlags {
|
||||||
private const val CATEGORIES = 0b00010
|
private const val CATEGORIES = 0b00010
|
||||||
private const val CUSTOM_COVER = 0b01000
|
private const val CUSTOM_COVER = 0b01000
|
||||||
private const val DELETE_DOWNLOADED = 0b10000
|
private const val DELETE_DOWNLOADED = 0b10000
|
||||||
|
private const val BOOKMARKS = 0b100000
|
||||||
|
|
||||||
private val coverCache: CoverCache by injectLazy()
|
private val coverCache: CoverCache by injectLazy()
|
||||||
private val downloadCache: DownloadCache by injectLazy()
|
private val downloadCache: DownloadCache by injectLazy()
|
||||||
|
private val getBookmarks: GetBookmarks by injectLazy()
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
fun hasChapters(value: Int): Boolean {
|
||||||
return value and CHAPTERS != 0
|
return value and CHAPTERS != 0
|
||||||
|
@ -50,6 +54,10 @@ object MigrationFlags {
|
||||||
return value and DELETE_DOWNLOADED != 0
|
return value and DELETE_DOWNLOADED != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasBookmarks(value: Int): Boolean {
|
||||||
|
return value and BOOKMARKS != 0
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns information about applicable flags with default selections. */
|
/** Returns information about applicable flags with default selections. */
|
||||||
fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List<MigrationFlag> {
|
fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List<MigrationFlag> {
|
||||||
val flags = mutableListOf<MigrationFlag>()
|
val flags = mutableListOf<MigrationFlag>()
|
||||||
|
@ -63,6 +71,9 @@ object MigrationFlags {
|
||||||
if (downloadCache.getDownloadCount(manga) > 0) {
|
if (downloadCache.getDownloadCount(manga) > 0) {
|
||||||
flags += MigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, MR.strings.delete_downloaded)
|
flags += MigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, MR.strings.delete_downloaded)
|
||||||
}
|
}
|
||||||
|
if (runBlocking { getBookmarks.await(manga.id) }.isNotEmpty()) {
|
||||||
|
flags += MigrationFlag.create(BOOKMARKS, defaultSelectedBitMap, MR.strings.label_bookmarks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,10 +36,15 @@ import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
|
import tachiyomi.domain.bookmark.interactor.DeleteBookmark
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmarks
|
||||||
|
import tachiyomi.domain.bookmark.interactor.SetBookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
|
@ -153,6 +158,9 @@ internal class MigrateDialogScreenModel(
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||||
|
private val getBookmarks: GetBookmarks = Injekt.get(),
|
||||||
|
private val setBookmark: SetBookmark = Injekt.get(),
|
||||||
|
private val deleteBookmark: DeleteBookmark = Injekt.get(),
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||||
private val getTracks: GetTracks = Injekt.get(),
|
private val getTracks: GetTracks = Injekt.get(),
|
||||||
|
@ -213,6 +221,7 @@ internal class MigrateDialogScreenModel(
|
||||||
val migrateCategories = MigrationFlags.hasCategories(flags)
|
val migrateCategories = MigrationFlags.hasCategories(flags)
|
||||||
val migrateCustomCover = MigrationFlags.hasCustomCover(flags)
|
val migrateCustomCover = MigrationFlags.hasCustomCover(flags)
|
||||||
val deleteDownloaded = MigrationFlags.hasDeleteDownloaded(flags)
|
val deleteDownloaded = MigrationFlags.hasDeleteDownloaded(flags)
|
||||||
|
val migrateBookmarks = MigrationFlags.hasBookmarks(flags)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
syncChaptersWithSource.await(sourceChapters, newManga, newSource)
|
syncChaptersWithSource.await(sourceChapters, newManga, newSource)
|
||||||
|
@ -229,7 +238,17 @@ internal class MigrateDialogScreenModel(
|
||||||
.filter { it.read }
|
.filter { it.read }
|
||||||
.maxOfOrNull { it.chapterNumber }
|
.maxOfOrNull { it.chapterNumber }
|
||||||
|
|
||||||
val updatedMangaChapters = mangaChapters.map { mangaChapter ->
|
val bookmarksByChapterId =
|
||||||
|
if (migrateBookmarks) {
|
||||||
|
getBookmarks.await(oldManga.id).groupBy { it.chapterId }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val updatedMangaChapters = mutableListOf<Chapter>()
|
||||||
|
val addedBookmarks = mutableListOf<Bookmark>()
|
||||||
|
|
||||||
|
mangaChapters.forEach { mangaChapter ->
|
||||||
var updatedChapter = mangaChapter
|
var updatedChapter = mangaChapter
|
||||||
if (updatedChapter.isRecognizedNumber) {
|
if (updatedChapter.isRecognizedNumber) {
|
||||||
val prevChapter = prevMangaChapters
|
val prevChapter = prevMangaChapters
|
||||||
|
@ -238,8 +257,27 @@ internal class MigrateDialogScreenModel(
|
||||||
if (prevChapter != null) {
|
if (prevChapter != null) {
|
||||||
updatedChapter = updatedChapter.copy(
|
updatedChapter = updatedChapter.copy(
|
||||||
dateFetch = prevChapter.dateFetch,
|
dateFetch = prevChapter.dateFetch,
|
||||||
bookmark = prevChapter.bookmark,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (migrateBookmarks) {
|
||||||
|
// Don't unbookmark anything, but copy existing bookmarks to updated.
|
||||||
|
if (prevChapter.bookmark) {
|
||||||
|
updatedChapter = updatedChapter.copy(bookmark = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarksByChapterId
|
||||||
|
?.get(prevChapter.id)
|
||||||
|
?.let { bookmarks ->
|
||||||
|
addedBookmarks.addAll(
|
||||||
|
bookmarks.map { bookmark ->
|
||||||
|
bookmark.copy(
|
||||||
|
mangaId = newManga.id,
|
||||||
|
chapterId = updatedChapter.id,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) {
|
if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) {
|
||||||
|
@ -247,11 +285,23 @@ internal class MigrateDialogScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedChapter
|
updatedMangaChapters.add(updatedChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapterUpdates = updatedMangaChapters.map { it.toChapterUpdate() }
|
val chapterUpdates = updatedMangaChapters.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
|
|
||||||
|
// Update bookmarks
|
||||||
|
if (migrateBookmarks) {
|
||||||
|
// Delete first, then insert/update in case manga is migrated into itself.
|
||||||
|
if (replace && bookmarksByChapterId?.isNotEmpty() == true) {
|
||||||
|
deleteBookmark.awaitAllByMangaId(oldManga.id, updateChapters = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedBookmarks.isNotEmpty()) {
|
||||||
|
setBookmark.awaitAll(addedBookmarks, updateChapters = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update categories
|
// Update categories
|
||||||
|
|
|
@ -57,13 +57,13 @@ import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.bookmark.interactor.DeleteBookmark
|
||||||
|
import tachiyomi.domain.bookmark.interactor.SetBookmark
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
||||||
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.NoChaptersException
|
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||||
import tachiyomi.domain.chapter.service.calculateChapterGap
|
import tachiyomi.domain.chapter.service.calculateChapterGap
|
||||||
import tachiyomi.domain.chapter.service.getChapterSort
|
import tachiyomi.domain.chapter.service.getChapterSort
|
||||||
|
@ -101,7 +101,6 @@ class MangaScreenModel(
|
||||||
private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(),
|
private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(),
|
||||||
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
|
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
|
||||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
|
@ -109,6 +108,8 @@ class MangaScreenModel(
|
||||||
private val addTracks: AddTracks = Injekt.get(),
|
private val addTracks: AddTracks = 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 setBookmark: SetBookmark = Injekt.get(),
|
||||||
|
private val deleteBookmark: DeleteBookmark = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
|
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
|
||||||
|
|
||||||
|
@ -737,11 +738,14 @@ class MangaScreenModel(
|
||||||
* @param chapters the list of chapters to bookmark.
|
* @param chapters the list of chapters to bookmark.
|
||||||
*/
|
*/
|
||||||
fun bookmarkChapters(chapters: List<Chapter>, bookmarked: Boolean) {
|
fun bookmarkChapters(chapters: List<Chapter>, bookmarked: Boolean) {
|
||||||
|
val toUpdate = chapters.filterNot { it.bookmark == bookmarked }
|
||||||
|
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
chapters
|
if (bookmarked) {
|
||||||
.filterNot { it.bookmark == bookmarked }
|
setBookmark.awaitByChapters(toUpdate)
|
||||||
.map { ChapterUpdate(id = it.id, bookmark = bookmarked) }
|
} else {
|
||||||
.let { updateChapter.awaitAll(it) }
|
deleteBookmark.awaitByChapters(toUpdate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
toggleAllSelection(false)
|
toggleAllSelection(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import eu.kanade.presentation.more.MoreScreen
|
||||||
import eu.kanade.presentation.util.Tab
|
import eu.kanade.presentation.util.Tab
|
||||||
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.ui.bookmarks.BookmarksTopScreen
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
|
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
|
||||||
|
@ -72,6 +73,7 @@ object MoreTab : Tab {
|
||||||
onClickDownloadQueue = { navigator.push(DownloadQueueScreen) },
|
onClickDownloadQueue = { navigator.push(DownloadQueueScreen) },
|
||||||
onClickCategories = { navigator.push(CategoryScreen()) },
|
onClickCategories = { navigator.push(CategoryScreen()) },
|
||||||
onClickStats = { navigator.push(StatsScreen()) },
|
onClickStats = { navigator.push(StatsScreen()) },
|
||||||
|
onClickBookmarks = { navigator.push(BookmarksTopScreen()) },
|
||||||
onClickDataAndStorage = { navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) },
|
onClickDataAndStorage = { navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) },
|
||||||
onClickSettings = { navigator.push(SettingsScreen()) },
|
onClickSettings = { navigator.push(SettingsScreen()) },
|
||||||
onClickAbout = { navigator.push(SettingsScreen(SettingsScreen.Destination.About)) },
|
onClickAbout = { navigator.push(SettingsScreen(SettingsScreen.Destination.About)) },
|
||||||
|
|
|
@ -96,10 +96,16 @@ import uy.kohesive.injekt.api.get
|
||||||
class ReaderActivity : BaseActivity() {
|
class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent {
|
fun newIntent(
|
||||||
|
context: Context,
|
||||||
|
mangaId: Long?,
|
||||||
|
chapterId: Long?,
|
||||||
|
pageIndex: Int? = null,
|
||||||
|
): Intent {
|
||||||
return Intent(context, ReaderActivity::class.java).apply {
|
return Intent(context, ReaderActivity::class.java).apply {
|
||||||
putExtra("manga", mangaId)
|
putExtra("manga", mangaId)
|
||||||
putExtra("chapter", chapterId)
|
putExtra("chapter", chapterId)
|
||||||
|
pageIndex?.let { page -> putExtra("page", page) }
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,10 +156,11 @@ class ReaderActivity : BaseActivity() {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val page = intent.extras?.getInt("page", -1) ?: -1
|
||||||
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
|
NotificationReceiver.dismissNotification(this, manga.hashCode(), Notifications.ID_NEW_CHAPTERS)
|
||||||
|
|
||||||
lifecycleScope.launchNonCancellable {
|
lifecycleScope.launchNonCancellable {
|
||||||
val initResult = viewModel.init(manga, chapter)
|
val initResult = viewModel.init(manga, chapter, page)
|
||||||
if (!initResult.getOrDefault(false)) {
|
if (!initResult.getOrDefault(false)) {
|
||||||
val exception = initResult.exceptionOrNull() ?: IllegalStateException("Unknown err")
|
val exception = initResult.exceptionOrNull() ?: IllegalStateException("Unknown err")
|
||||||
withUIContext {
|
withUIContext {
|
||||||
|
@ -448,6 +455,9 @@ class ReaderActivity : BaseActivity() {
|
||||||
onSetAsCover = viewModel::setAsCover,
|
onSetAsCover = viewModel::setAsCover,
|
||||||
onShare = viewModel::shareImage,
|
onShare = viewModel::shareImage,
|
||||||
onSave = viewModel::saveImage,
|
onSave = viewModel::saveImage,
|
||||||
|
onBookmarkPage = viewModel::updateCurrentPageBookmark,
|
||||||
|
onUnbookmarkPage = viewModel::deleteCurrentPageBookmark,
|
||||||
|
getPageBookmark = viewModel::getPageBookmark,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
|
|
|
@ -60,6 +60,10 @@ import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.bookmark.interactor.DeleteBookmark
|
||||||
|
import tachiyomi.domain.bookmark.interactor.GetBookmark
|
||||||
|
import tachiyomi.domain.bookmark.interactor.SetBookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
|
@ -97,6 +101,9 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
private val getNextChapters: GetNextChapters = Injekt.get(),
|
private val getNextChapters: GetNextChapters = Injekt.get(),
|
||||||
private val upsertHistory: UpsertHistory = Injekt.get(),
|
private val upsertHistory: UpsertHistory = Injekt.get(),
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||||
|
private val setBookmark: SetBookmark = Injekt.get(),
|
||||||
|
private val deleteBookmark: DeleteBookmark = Injekt.get(),
|
||||||
|
private val getBookmark: GetBookmark = Injekt.get(),
|
||||||
private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(),
|
private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(),
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
@ -255,10 +262,15 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will
|
* Initializes this presenter with the given [mangaId], [initialChapterId] and [pageIndex].
|
||||||
* fetch the manga from the database and initialize the initial chapter.
|
* [pageIndex] is optional, if provided, reader will open that page.
|
||||||
|
* This method will fetch the manga from the database and initialize the initial chapter.
|
||||||
*/
|
*/
|
||||||
suspend fun init(mangaId: Long, initialChapterId: Long): Result<Boolean> {
|
suspend fun init(
|
||||||
|
mangaId: Long,
|
||||||
|
initialChapterId: Long,
|
||||||
|
pageIndex: Int? = null,
|
||||||
|
): Result<Boolean> {
|
||||||
if (!needsInit()) return Result.success(true)
|
if (!needsInit()) return Result.success(true)
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
try {
|
try {
|
||||||
|
@ -271,7 +283,15 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
|
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
|
||||||
|
|
||||||
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
val chapter = chapterList.first { chapterId == it.chapter.id }
|
||||||
|
// TODO: this is hacky solution, what is proper?
|
||||||
|
// Don't update requestedPage if it's already >= 0, as initialized as -1.
|
||||||
|
// But maybe it's sometimes not -1 but expected to be updated?
|
||||||
|
if (pageIndex != null && pageIndex >= 0) {
|
||||||
|
chapter.requestedPage = pageIndex
|
||||||
|
chapter.chapter.last_page_read = pageIndex
|
||||||
|
}
|
||||||
|
loadChapter(loader!!, chapter)
|
||||||
Result.success(true)
|
Result.success(true)
|
||||||
} else {
|
} else {
|
||||||
// Unlikely but okay
|
// Unlikely but okay
|
||||||
|
@ -611,12 +631,11 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
chapter.bookmark = bookmarked
|
chapter.bookmark = bookmarked
|
||||||
|
|
||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
updateChapter.await(
|
if (bookmarked) {
|
||||||
ChapterUpdate(
|
setBookmark.await(chapter.manga_id!!.toLong(), chapter.id!!.toLong(), null, null)
|
||||||
id = chapter.id!!.toLong(),
|
} else {
|
||||||
bookmark = bookmarked,
|
deleteBookmark.await(chapter.manga_id!!.toLong(), chapter.id!!.toLong(), null)
|
||||||
),
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
|
@ -626,6 +645,40 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or updates a page bookmark for a page selected in page-actions dialog.
|
||||||
|
*/
|
||||||
|
fun updateCurrentPageBookmark(bookmarkNote: String) {
|
||||||
|
val manga = manga ?: return
|
||||||
|
val page = (state.value.dialog as? Dialog.PageActions)?.page ?: return
|
||||||
|
|
||||||
|
viewModelScope.launchNonCancellable {
|
||||||
|
setBookmark.await(manga.id, chapterId, page.index, bookmarkNote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the bookmark for the currently selected page.
|
||||||
|
*/
|
||||||
|
fun deleteCurrentPageBookmark() {
|
||||||
|
val manga = manga ?: return
|
||||||
|
val page = (state.value.dialog as? Dialog.PageActions)?.page ?: return
|
||||||
|
|
||||||
|
viewModelScope.launchNonCancellable {
|
||||||
|
deleteBookmark.await(manga.id, chapterId, page.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to retrieve the bookmark for the currently selected page.
|
||||||
|
* @return The bookmark for the current page, or null if it doesn't exist.
|
||||||
|
*/
|
||||||
|
fun getPageBookmark(): Bookmark? {
|
||||||
|
val manga = manga ?: return null
|
||||||
|
val page = (state.value.dialog as? Dialog.PageActions)?.page ?: return null
|
||||||
|
return runBlocking { getBookmark.await(manga.id, chapterId, page.index) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the viewer position used by this manga or the default one.
|
* Returns the viewer position used by this manga or the default one.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -35,9 +35,11 @@ import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.bookmark.interactor.DeleteBookmark
|
||||||
|
import tachiyomi.domain.bookmark.interactor.SetBookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkDelete
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
|
||||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
@ -52,12 +54,13 @@ class UpdatesScreenModel(
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val downloadCache: DownloadCache = Injekt.get(),
|
private val downloadCache: DownloadCache = Injekt.get(),
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
|
||||||
private val setReadStatus: SetReadStatus = Injekt.get(),
|
private val setReadStatus: SetReadStatus = Injekt.get(),
|
||||||
private val getUpdates: GetUpdates = Injekt.get(),
|
private val getUpdates: GetUpdates = Injekt.get(),
|
||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
|
private val setBookmark: SetBookmark = Injekt.get(),
|
||||||
|
private val deleteBookmark: DeleteBookmark = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
||||||
|
|
||||||
|
@ -213,11 +216,21 @@ class UpdatesScreenModel(
|
||||||
* @param updates the list of chapters to bookmark.
|
* @param updates the list of chapters to bookmark.
|
||||||
*/
|
*/
|
||||||
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
fun bookmarkUpdates(updates: List<UpdatesItem>, bookmark: Boolean) {
|
||||||
|
val toUpdate = updates.filterNot { it.update.bookmark == bookmark }
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
updates
|
if (bookmark) {
|
||||||
.filterNot { it.update.bookmark == bookmark }
|
toUpdate
|
||||||
.map { ChapterUpdate(id = it.update.chapterId, bookmark = bookmark) }
|
.map {
|
||||||
.let { updateChapter.awaitAll(it) }
|
Bookmark.create().copy(mangaId = it.update.mangaId, chapterId = it.update.chapterId)
|
||||||
|
}
|
||||||
|
.let { setBookmark.awaitAll(it) }
|
||||||
|
} else {
|
||||||
|
toUpdate
|
||||||
|
.map {
|
||||||
|
BookmarkDelete(mangaId = it.update.mangaId, chapterId = it.update.chapterId)
|
||||||
|
}
|
||||||
|
.let { deleteBookmark.awaitAll(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
toggleAllSelection(false)
|
toggleAllSelection(false)
|
||||||
}
|
}
|
||||||
|
|
94
data/src/main/java/tachiyomi/data/bookmark/BookmarkMapper.kt
Normal file
94
data/src/main/java/tachiyomi/data/bookmark/BookmarkMapper.kt
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package tachiyomi.data.bookmark
|
||||||
|
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkWithChapterNumber
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkedPage
|
||||||
|
import tachiyomi.domain.bookmark.model.MangaWithBookmarks
|
||||||
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
|
|
||||||
|
object BookmarkMapper {
|
||||||
|
fun mapBookmark(
|
||||||
|
id: Long,
|
||||||
|
mangaId: Long,
|
||||||
|
chapterId: Long,
|
||||||
|
pageIndex: Long?,
|
||||||
|
note: String?,
|
||||||
|
lastModifiedAt: Long,
|
||||||
|
): Bookmark = Bookmark(
|
||||||
|
id = id,
|
||||||
|
mangaId = mangaId,
|
||||||
|
chapterId = chapterId,
|
||||||
|
pageIndex = pageIndex?.toInt(),
|
||||||
|
note = note,
|
||||||
|
lastModifiedAt = lastModifiedAt * 1000L,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun mapMangaWithBookmarks(
|
||||||
|
mangaId: Long,
|
||||||
|
mangaTitle: String,
|
||||||
|
mangaThumbnailUrl: String?,
|
||||||
|
mangaSource: Long,
|
||||||
|
isMangaFavorite: Boolean,
|
||||||
|
mangaCoverLastModified: Long,
|
||||||
|
numberOfBookmarks: Long,
|
||||||
|
bookmarkLastModified: Long?,
|
||||||
|
): MangaWithBookmarks = MangaWithBookmarks(
|
||||||
|
mangaId = mangaId,
|
||||||
|
mangaTitle = mangaTitle,
|
||||||
|
numberOfBookmarks = numberOfBookmarks,
|
||||||
|
bookmarkLastModified = (bookmarkLastModified ?: 0L) * 1000L,
|
||||||
|
coverData = MangaCover(
|
||||||
|
mangaId = mangaId,
|
||||||
|
sourceId = mangaSource,
|
||||||
|
isMangaFavorite = isMangaFavorite,
|
||||||
|
url = mangaThumbnailUrl,
|
||||||
|
lastModified = mangaCoverLastModified,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun mapBookmarkedPage(
|
||||||
|
bookmarkId: Long,
|
||||||
|
mangaId: Long,
|
||||||
|
chapterId: Long,
|
||||||
|
pageIndex: Long?,
|
||||||
|
mangaTitle: String,
|
||||||
|
mangaThumbnailUrl: String?,
|
||||||
|
mangaSource: Long,
|
||||||
|
isMangaFavorite: Boolean,
|
||||||
|
mangaCoverLastModified: Long,
|
||||||
|
chapterNumber: Double,
|
||||||
|
chapterName: String,
|
||||||
|
note: String?,
|
||||||
|
lastModifiedAt: Long,
|
||||||
|
): BookmarkedPage = BookmarkedPage(
|
||||||
|
bookmarkId = bookmarkId,
|
||||||
|
mangaId = mangaId,
|
||||||
|
chapterId = chapterId,
|
||||||
|
pageIndex = pageIndex?.toInt(),
|
||||||
|
mangaTitle = mangaTitle,
|
||||||
|
chapterNumber = chapterNumber,
|
||||||
|
chapterName = chapterName,
|
||||||
|
note = note,
|
||||||
|
lastModifiedAt = lastModifiedAt * 1000L,
|
||||||
|
coverData = MangaCover(
|
||||||
|
mangaId = mangaId,
|
||||||
|
sourceId = mangaSource,
|
||||||
|
isMangaFavorite = isMangaFavorite,
|
||||||
|
url = mangaThumbnailUrl,
|
||||||
|
lastModified = mangaCoverLastModified,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun mapBookmarkWithChapterNumber(
|
||||||
|
@Suppress("UNUSED_PARAMETER") chapterUrl: String,
|
||||||
|
chapterNumber: Double,
|
||||||
|
pageIndex: Long?,
|
||||||
|
note: String?,
|
||||||
|
lastModifiedAt: Long,
|
||||||
|
): BookmarkWithChapterNumber = BookmarkWithChapterNumber(
|
||||||
|
chapterNumber = chapterNumber,
|
||||||
|
pageIndex = pageIndex?.toInt(),
|
||||||
|
note = note,
|
||||||
|
lastModifiedAt = lastModifiedAt * 1000L,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package tachiyomi.data.bookmark
|
||||||
|
|
||||||
|
import tachiyomi.data.Database
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkDelete
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkUpdate
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkWithChapterNumber
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkedPage
|
||||||
|
import tachiyomi.domain.bookmark.model.MangaWithBookmarks
|
||||||
|
import tachiyomi.domain.bookmark.repository.BookmarkRepository
|
||||||
|
|
||||||
|
class BookmarkRepositoryImpl(
|
||||||
|
private val handler: DatabaseHandler,
|
||||||
|
) : BookmarkRepository {
|
||||||
|
override suspend fun get(id: Long): Bookmark? {
|
||||||
|
return handler.awaitOneOrNull { bookmarksQueries.getBookmarkById(id, BookmarkMapper::mapBookmark) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun get(mangaId: Long, chapterId: Long, pageIndex: Int?): Bookmark? {
|
||||||
|
return handler.awaitOneOrNull {
|
||||||
|
bookmarksQueries.getBookmarkByMangaAndChapterPage(
|
||||||
|
mangaId,
|
||||||
|
chapterId,
|
||||||
|
pageIndex?.toLong(),
|
||||||
|
BookmarkMapper::mapBookmark,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAllByMangaId(mangaId: Long): List<Bookmark> {
|
||||||
|
return handler.awaitList {
|
||||||
|
bookmarksQueries.getAllByMangaId(mangaId, BookmarkMapper::mapBookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaWithBookmarks(): List<MangaWithBookmarks> {
|
||||||
|
return handler.awaitList {
|
||||||
|
mangaWithBookmarksViewQueries.mangaWithBookmarks(BookmarkMapper::mapMangaWithBookmarks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getBookmarkedPagesByMangaId(mangaId: Long): List<BookmarkedPage> {
|
||||||
|
return handler.awaitList {
|
||||||
|
bookmarksViewQueries.getBookmarksByManga(mangaId, BookmarkMapper::mapBookmarkedPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getWithChapterNumberByMangaId(mangaId: Long): List<BookmarkWithChapterNumber> {
|
||||||
|
return handler.awaitList {
|
||||||
|
bookmarksQueries.getWithChapterInfoByMangaId(mangaId, BookmarkMapper::mapBookmarkWithChapterNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun insert(bookmark: Bookmark) {
|
||||||
|
handler.await {
|
||||||
|
bookmarksQueries.insert(
|
||||||
|
mangaId = bookmark.mangaId,
|
||||||
|
chapterId = bookmark.chapterId,
|
||||||
|
pageIndex = bookmark.pageIndex?.toLong(),
|
||||||
|
note = bookmark.note,
|
||||||
|
// Seconds in DB.
|
||||||
|
lastModifiedAt = bookmark.lastModifiedAt / 1000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updatePartial(update: BookmarkUpdate) {
|
||||||
|
handler.await {
|
||||||
|
bookmarksQueries.update(
|
||||||
|
bookmarkId = update.id,
|
||||||
|
note = update.note,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun insertOrReplaceAll(
|
||||||
|
idsToDelete: List<Long>,
|
||||||
|
bookmarksToAdd: List<Bookmark>,
|
||||||
|
) {
|
||||||
|
handler.await(inTransaction = true) {
|
||||||
|
idsToDelete.forEach { bookmarkId -> bookmarksQueries.delete(bookmarkId) }
|
||||||
|
|
||||||
|
bookmarksToAdd.forEach { bookmark ->
|
||||||
|
bookmarksQueries.insert(
|
||||||
|
mangaId = bookmark.mangaId,
|
||||||
|
chapterId = bookmark.chapterId,
|
||||||
|
pageIndex = bookmark.pageIndex?.toLong(),
|
||||||
|
note = bookmark.note,
|
||||||
|
lastModifiedAt = bookmark.lastModifiedAt / 1000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(bookmarkId: Long) {
|
||||||
|
handler.await { bookmarksQueries.delete(bookmarkId = bookmarkId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(delete: BookmarkDelete) {
|
||||||
|
handler.await { deleteBlocking(delete) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteAll(delete: List<BookmarkDelete>) {
|
||||||
|
handler.await(inTransaction = true) {
|
||||||
|
for (bookmark in delete) {
|
||||||
|
deleteBlocking(bookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteAllByMangaId(mangaId: Long) {
|
||||||
|
handler.await { bookmarksQueries.deleteAllByMangaId(mangaId = mangaId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Database.deleteBlocking(delete: BookmarkDelete) {
|
||||||
|
bookmarksQueries.deleteByMangaAndChapterPage(
|
||||||
|
mangaId = delete.mangaId,
|
||||||
|
chapterId = delete.chapterId,
|
||||||
|
pageIndex = delete.pageIndex?.toLong(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
77
data/src/main/sqldelight/tachiyomi/data/bookmarks.sq
Normal file
77
data/src/main/sqldelight/tachiyomi/data/bookmarks.sq
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
CREATE TABLE bookmarks(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
chapter_id INTEGER NOT NULL,
|
||||||
|
page_index INTEGER,
|
||||||
|
note TEXT,
|
||||||
|
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Only single bookmark per page is allowed.
|
||||||
|
CREATE UNIQUE INDEX bookmark_manga_id_chapter_id_page_index
|
||||||
|
ON bookmarks(manga_id, chapter_id, page_index);
|
||||||
|
|
||||||
|
-- For chapters FK with DELETE CASCADE.
|
||||||
|
CREATE INDEX bookmark_chapter_id ON bookmarks(chapter_id);
|
||||||
|
|
||||||
|
CREATE TRIGGER update_last_modified_at_bookmarks
|
||||||
|
AFTER UPDATE ON bookmarks
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE bookmarks
|
||||||
|
SET last_modified_at = strftime('%s', 'now')
|
||||||
|
WHERE _id = new._id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Methods
|
||||||
|
getBookmarkById:
|
||||||
|
SELECT *
|
||||||
|
FROM bookmarks
|
||||||
|
WHERE _id = :id;
|
||||||
|
|
||||||
|
getAllByMangaId:
|
||||||
|
SELECT *
|
||||||
|
FROM bookmarks
|
||||||
|
WHERE manga_id = :mangaId;
|
||||||
|
|
||||||
|
getWithChapterInfoByMangaId:
|
||||||
|
SELECT
|
||||||
|
chapters.url,
|
||||||
|
chapters.chapter_number,
|
||||||
|
bookmarks.page_index,
|
||||||
|
bookmarks.note,
|
||||||
|
bookmarks.last_modified_at
|
||||||
|
FROM bookmarks JOIN chapters ON chapters._id = bookmarks.chapter_id
|
||||||
|
WHERE bookmarks.manga_id = :mangaId;
|
||||||
|
|
||||||
|
getBookmarkByMangaAndChapterPage:
|
||||||
|
SELECT *
|
||||||
|
FROM bookmarks
|
||||||
|
WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page_index = :pageIndex;
|
||||||
|
|
||||||
|
insert:
|
||||||
|
INSERT INTO bookmarks(manga_id, chapter_id, page_index, note, last_modified_at)
|
||||||
|
VALUES (:mangaId, :chapterId, :pageIndex, :note, :lastModifiedAt);
|
||||||
|
|
||||||
|
update:
|
||||||
|
UPDATE bookmarks SET
|
||||||
|
note = coalesce(:note, note)
|
||||||
|
WHERE _id = :bookmarkId;
|
||||||
|
|
||||||
|
delete:
|
||||||
|
DELETE FROM bookmarks
|
||||||
|
WHERE _id = :bookmarkId;
|
||||||
|
|
||||||
|
deleteByMangaAndChapterPage:
|
||||||
|
DELETE FROM bookmarks
|
||||||
|
WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page_index = :pageIndex;
|
||||||
|
|
||||||
|
deleteAllByMangaId:
|
||||||
|
DELETE FROM bookmarks
|
||||||
|
WHERE manga_id = :mangaId;
|
80
data/src/main/sqldelight/tachiyomi/migrations/28.sqm
Normal file
80
data/src/main/sqldelight/tachiyomi/migrations/28.sqm
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
-- Tables and views for page bookmars with notes.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bookmarks(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
chapter_id INTEGER NOT NULL,
|
||||||
|
page_index INTEGER,
|
||||||
|
note TEXT,
|
||||||
|
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Only single bookmark per page is allowed.
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS bookmark_manga_id_chapter_id_page_index
|
||||||
|
ON bookmarks(manga_id, chapter_id, page_index);
|
||||||
|
|
||||||
|
-- For chapters FK with DELETE CASCADE.
|
||||||
|
CREATE INDEX IF NOT EXISTS bookmark_chapter_id ON bookmarks(chapter_id);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS update_last_modified_at_bookmarks
|
||||||
|
AFTER UPDATE ON bookmarks
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE bookmarks
|
||||||
|
SET last_modified_at = strftime('%s', 'now')
|
||||||
|
WHERE _id = new._id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
|
||||||
|
DROP VIEW IF EXISTS bookmarksView;
|
||||||
|
|
||||||
|
CREATE VIEW bookmarksView AS
|
||||||
|
SELECT
|
||||||
|
bookmarks._id AS bookmarkId,
|
||||||
|
mangas._id AS mangaId,
|
||||||
|
chapters._id AS chapterId,
|
||||||
|
bookmarks.page_index AS pageIndex,
|
||||||
|
mangas.title AS mangaTitle,
|
||||||
|
mangas.thumbnail_url AS mangaThumbnailUrl,
|
||||||
|
mangas.source AS mangaSource,
|
||||||
|
mangas.favorite AS isMangaFavorite,
|
||||||
|
mangas.cover_last_modified AS mangaCoverLastModified,
|
||||||
|
chapters.chapter_number AS chapterNumber,
|
||||||
|
chapters.name AS chapterName,
|
||||||
|
bookmarks.note,
|
||||||
|
bookmarks.last_modified_at AS lastModifiedAt
|
||||||
|
FROM bookmarks
|
||||||
|
JOIN mangas ON mangas._id = bookmarks.manga_id
|
||||||
|
JOIN chapters ON chapters._id = bookmarks.chapter_id
|
||||||
|
ORDER BY mangaTitle, chapterNumber ASC, pageIndex ASC;
|
||||||
|
|
||||||
|
|
||||||
|
DROP VIEW IF EXISTS mangaWithBookmarksView;
|
||||||
|
|
||||||
|
CREATE VIEW mangaWithBookmarksView AS
|
||||||
|
SELECT
|
||||||
|
mangas._id AS mangaId,
|
||||||
|
mangas.title AS mangaTitle,
|
||||||
|
mangas.thumbnail_url AS mangaThumbnailUrl,
|
||||||
|
mangas.source AS mangaSource,
|
||||||
|
mangas.favorite AS isMangaFavorite,
|
||||||
|
mangas.cover_last_modified AS mangaCoverLastModified,
|
||||||
|
COUNT(*) AS numberOfBookmarks,
|
||||||
|
MAX(bookmarks.last_modified_at) AS bookmarkLastModified
|
||||||
|
FROM bookmarks
|
||||||
|
JOIN mangas ON mangas._id = bookmarks.manga_id
|
||||||
|
WHERE mangas.favorite = 1
|
||||||
|
GROUP BY mangas._id
|
||||||
|
ORDER BY numberOfBookmarks DESC;
|
||||||
|
|
||||||
|
-- One-time insert new records for all bookmarked chapters.
|
||||||
|
INSERT INTO bookmarks (manga_id, chapter_id, page_index, note, last_modified_at)
|
||||||
|
SELECT manga_id, _id, NULL, NULL, last_modified_at
|
||||||
|
FROM chapters
|
||||||
|
WHERE bookmark = 1 AND NOT EXISTS (SELECT * FROM bookmarks WHERE bookmarks.page_index IS NULL);
|
29
data/src/main/sqldelight/tachiyomi/view/bookmarksView.sq
Normal file
29
data/src/main/sqldelight/tachiyomi/view/bookmarksView.sq
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
CREATE VIEW bookmarksView AS
|
||||||
|
SELECT
|
||||||
|
bookmarks._id AS bookmarkId,
|
||||||
|
mangas._id AS mangaId,
|
||||||
|
chapters._id AS chapterId,
|
||||||
|
bookmarks.page_index AS pageIndex,
|
||||||
|
mangas.title AS mangaTitle,
|
||||||
|
mangas.thumbnail_url AS mangaThumbnailUrl,
|
||||||
|
mangas.source AS mangaSource,
|
||||||
|
mangas.favorite AS isMangaFavorite,
|
||||||
|
mangas.cover_last_modified AS mangaCoverLastModified,
|
||||||
|
chapters.chapter_number AS chapterNumber,
|
||||||
|
chapters.name AS chapterName,
|
||||||
|
bookmarks.note,
|
||||||
|
bookmarks.last_modified_at AS lastModifiedAt
|
||||||
|
FROM bookmarks
|
||||||
|
JOIN mangas ON mangas._id = bookmarks.manga_id
|
||||||
|
JOIN chapters ON chapters._id = bookmarks.chapter_id
|
||||||
|
ORDER BY mangaTitle, chapterNumber ASC, pageIndex ASC;
|
||||||
|
|
||||||
|
getBookmarksByManga:
|
||||||
|
SELECT *
|
||||||
|
FROM bookmarksView
|
||||||
|
WHERE mangaId = :mangaId;
|
||||||
|
|
||||||
|
getBookmarksByNotePattern:
|
||||||
|
SELECT *
|
||||||
|
FROM bookmarksView
|
||||||
|
WHERE note LIKE :notePattern;
|
|
@ -0,0 +1,19 @@
|
||||||
|
CREATE VIEW mangaWithBookmarksView AS
|
||||||
|
SELECT
|
||||||
|
mangas._id AS mangaId,
|
||||||
|
mangas.title AS mangaTitle,
|
||||||
|
mangas.thumbnail_url AS mangaThumbnailUrl,
|
||||||
|
mangas.source AS mangaSource,
|
||||||
|
mangas.favorite AS isMangaFavorite,
|
||||||
|
mangas.cover_last_modified AS mangaCoverLastModified,
|
||||||
|
COUNT(*) AS numberOfBookmarks,
|
||||||
|
MAX(bookmarks.last_modified_at) AS bookmarkLastModified
|
||||||
|
FROM bookmarks
|
||||||
|
JOIN mangas ON mangas._id = bookmarks.manga_id
|
||||||
|
WHERE mangas.favorite = 1
|
||||||
|
GROUP BY mangas._id
|
||||||
|
ORDER BY numberOfBookmarks DESC;
|
||||||
|
|
||||||
|
mangaWithBookmarks:
|
||||||
|
SELECT *
|
||||||
|
FROM mangaWithBookmarksView;
|
|
@ -0,0 +1,89 @@
|
||||||
|
package tachiyomi.domain.bookmark.interactor
|
||||||
|
|
||||||
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkDelete
|
||||||
|
import tachiyomi.domain.bookmark.repository.BookmarkRepository
|
||||||
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
|
|
||||||
|
class DeleteBookmark(
|
||||||
|
private val bookmarkRepository: BookmarkRepository,
|
||||||
|
private val chapterRepository: ChapterRepository,
|
||||||
|
) {
|
||||||
|
suspend fun await(mangaId: Long, chapterId: Long, pageIndex: Int?) = withNonCancellableContext {
|
||||||
|
try {
|
||||||
|
if (pageIndex == null) {
|
||||||
|
chapterRepository.update(ChapterUpdate.bookmarkUpdate(chapterId, false))
|
||||||
|
}
|
||||||
|
bookmarkRepository.delete(
|
||||||
|
BookmarkDelete(
|
||||||
|
mangaId = mangaId,
|
||||||
|
chapterId = chapterId,
|
||||||
|
pageIndex = pageIndex,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Result.Success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
return@withNonCancellableContext Result.InternalError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun awaitAll(delete: List<BookmarkDelete>) = withNonCancellableContext {
|
||||||
|
try {
|
||||||
|
// Not in transaction, but chapters first
|
||||||
|
// to not to affect existing chapter-level bookmarks.
|
||||||
|
val chapters = delete
|
||||||
|
.mapNotNull {
|
||||||
|
when (it.pageIndex) {
|
||||||
|
null -> ChapterUpdate.bookmarkUpdate(it.chapterId, false)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
chapterRepository.updateAll(chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkRepository.deleteAll(delete)
|
||||||
|
|
||||||
|
Result.Success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
return@withNonCancellableContext Result.InternalError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun awaitByChapters(chaptersToUnbookmark: List<Chapter>) {
|
||||||
|
return chaptersToUnbookmark
|
||||||
|
.map { BookmarkDelete(mangaId = it.mangaId, chapterId = it.id) }
|
||||||
|
.let { awaitAll(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun awaitAllByMangaId(mangaId: Long, updateChapters: Boolean = true) =
|
||||||
|
withNonCancellableContext {
|
||||||
|
try {
|
||||||
|
if (updateChapters) {
|
||||||
|
val unbookmarkChapters =
|
||||||
|
chapterRepository.getBookmarkedChaptersByMangaId(mangaId)
|
||||||
|
.map { ChapterUpdate.bookmarkUpdate(id = it.id, bookmark = false) }
|
||||||
|
if (unbookmarkChapters.isNotEmpty()) {
|
||||||
|
chapterRepository.updateAll(unbookmarkChapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkRepository.deleteAllByMangaId(mangaId)
|
||||||
|
Result.Success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
return@withNonCancellableContext Result.InternalError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Result {
|
||||||
|
object Success : Result()
|
||||||
|
data class InternalError(val error: Throwable) : Result()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package tachiyomi.domain.bookmark.interactor
|
||||||
|
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
|
import tachiyomi.domain.bookmark.repository.BookmarkRepository
|
||||||
|
|
||||||
|
class GetBookmark(
|
||||||
|
private val bookmarkRepository: BookmarkRepository,
|
||||||
|
) {
|
||||||
|
suspend fun await(mangaId: Long, chapterId: Long, pageIndex: Int): Bookmark? {
|
||||||
|
return bookmarkRepository.get(mangaId, chapterId, pageIndex)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package tachiyomi.domain.bookmark.interactor
|
||||||
|
|
||||||
|
import tachiyomi.domain.bookmark.repository.BookmarkRepository
|
||||||
|
|
||||||
|
class GetBookmarkedMangas(
|
||||||
|
private val bookmarkRepository: BookmarkRepository,
|
||||||
|
) {
|
||||||
|
suspend fun await() = run { bookmarkRepository.getMangaWithBookmarks() }
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package tachiyomi.domain.bookmark.interactor
|
||||||
|
|
||||||
|
import tachiyomi.domain.bookmark.repository.BookmarkRepository
|
||||||
|
|
||||||
|
class GetBookmarkedPages(
|
||||||
|
private val bookmarkRepository: BookmarkRepository,
|
||||||
|
) {
|
||||||
|
suspend fun await(mangaId: Long) =
|
||||||
|
run { bookmarkRepository.getBookmarkedPagesByMangaId(mangaId) }
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package tachiyomi.domain.bookmark.interactor
|
||||||
|
|
||||||
|
import tachiyomi.domain.bookmark.repository.BookmarkRepository
|
||||||
|
|
||||||
|
class GetBookmarks(
|
||||||
|
private val bookmarkRepository: BookmarkRepository,
|
||||||
|
) {
|
||||||
|
suspend fun await(mangaId: Long) =
|
||||||
|
run { bookmarkRepository.getAllByMangaId(mangaId) }
|
||||||
|
|
||||||
|
suspend fun awaitWithChapterNumbers(mangaId: Long) =
|
||||||
|
run { bookmarkRepository.getWithChapterNumberByMangaId(mangaId) }
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package tachiyomi.domain.bookmark.interactor
|
||||||
|
|
||||||
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkUpdate
|
||||||
|
import tachiyomi.domain.bookmark.repository.BookmarkRepository
|
||||||
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class SetBookmark(
|
||||||
|
private val bookmarkRepository: BookmarkRepository,
|
||||||
|
private val chapterRepository: ChapterRepository,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Inserts a new bookmark or updates the note of an existing bookmark if one already exists
|
||||||
|
* for the given manga, chapter, and page index.
|
||||||
|
* Ensures that at most one bookmark per page exists.
|
||||||
|
* Updates correspondent chapter bookmark field.
|
||||||
|
*
|
||||||
|
* @param pageIndex when null, bookmark is considered as chapter bookmark.
|
||||||
|
* @param note Optional text note to be associated with the bookmark.
|
||||||
|
*/
|
||||||
|
suspend fun await(
|
||||||
|
mangaId: Long,
|
||||||
|
chapterId: Long,
|
||||||
|
pageIndex: Int?,
|
||||||
|
note: String?,
|
||||||
|
lastModifiedAt: Long? = null,
|
||||||
|
): Result =
|
||||||
|
withNonCancellableContext {
|
||||||
|
try {
|
||||||
|
if (pageIndex == null) {
|
||||||
|
chapterRepository.update(ChapterUpdate.bookmarkUpdate(chapterId, true))
|
||||||
|
}
|
||||||
|
val existingBookmark = bookmarkRepository.get(mangaId, chapterId, pageIndex)
|
||||||
|
if (existingBookmark != null) {
|
||||||
|
bookmarkRepository.updatePartial(
|
||||||
|
BookmarkUpdate(
|
||||||
|
id = existingBookmark.id,
|
||||||
|
note = note,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val newBookmark =
|
||||||
|
Bookmark.create()
|
||||||
|
.copy(
|
||||||
|
mangaId = mangaId,
|
||||||
|
chapterId = chapterId,
|
||||||
|
pageIndex = pageIndex,
|
||||||
|
note = note,
|
||||||
|
lastModifiedAt = lastModifiedAt ?: Date().time,
|
||||||
|
)
|
||||||
|
bookmarkRepository.insert(newBookmark)
|
||||||
|
}
|
||||||
|
Result.Success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
Result.InternalError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates or inserts new bookmarks
|
||||||
|
* By default updates correspondent chapter bookmark field.
|
||||||
|
*/
|
||||||
|
suspend fun awaitAll(addedBookmarks: List<Bookmark>, updateChapters: Boolean = true): Result {
|
||||||
|
return try {
|
||||||
|
if (updateChapters) {
|
||||||
|
val chapters = addedBookmarks
|
||||||
|
.mapNotNull {
|
||||||
|
when (it.pageIndex) {
|
||||||
|
null -> ChapterUpdate.bookmarkUpdate(it.chapterId, true)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
chapterRepository.updateAll(chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check what should be removed to avoid duplication and when update can be skipped.
|
||||||
|
val oldBookmarks = addedBookmarks
|
||||||
|
.map { it.mangaId }
|
||||||
|
.distinct()
|
||||||
|
.flatMap { mangaId ->
|
||||||
|
bookmarkRepository.getAllByMangaId(mangaId)
|
||||||
|
}
|
||||||
|
.associate { Triple(it.mangaId, it.chapterId, it.pageIndex) to it.id }
|
||||||
|
|
||||||
|
val idsToDelete = mutableListOf<Long>()
|
||||||
|
val toAdd = mutableListOf<Bookmark>()
|
||||||
|
|
||||||
|
addedBookmarks.forEach { bookmark ->
|
||||||
|
val oldBookmarkId =
|
||||||
|
oldBookmarks[Triple(bookmark.mangaId, bookmark.chapterId, bookmark.pageIndex)]
|
||||||
|
// Don't delete & insert if there's already an old bookmark and new has no note set.
|
||||||
|
// However, this prevents merging or backup restores to remove existing notes.
|
||||||
|
// If needed, then add `bookmark.pageIndex == null` to only check for chapter bookmarks.
|
||||||
|
val isUpdateNeeded = oldBookmarkId == null || bookmark.note?.isNotBlank() ?: false
|
||||||
|
|
||||||
|
if (isUpdateNeeded) {
|
||||||
|
oldBookmarkId?.let { idsToDelete.add(it) }
|
||||||
|
toAdd.add(bookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toAdd.isNotEmpty()) {
|
||||||
|
bookmarkRepository.insertOrReplaceAll(idsToDelete, toAdd)
|
||||||
|
}
|
||||||
|
Result.Success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
Result.InternalError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun awaitByChapters(chaptersToBookmark: List<Chapter>): Result {
|
||||||
|
return chaptersToBookmark
|
||||||
|
.map { Bookmark.create().copy(mangaId = it.mangaId, chapterId = it.id) }
|
||||||
|
.let { awaitAll(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Result {
|
||||||
|
object Success : Result()
|
||||||
|
data class InternalError(val error: Throwable) : Result()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package tachiyomi.domain.bookmark.model
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
data class Bookmark(
|
||||||
|
val id: Long,
|
||||||
|
val mangaId: Long,
|
||||||
|
val chapterId: Long,
|
||||||
|
/**
|
||||||
|
* null is for chapter-level bookmark. Currently only non-null values are supported.
|
||||||
|
*/
|
||||||
|
val pageIndex: Int?,
|
||||||
|
val note: String?,
|
||||||
|
val lastModifiedAt: Long,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create() = Bookmark(
|
||||||
|
id = -1,
|
||||||
|
mangaId = -1,
|
||||||
|
chapterId = -1,
|
||||||
|
pageIndex = null,
|
||||||
|
note = null,
|
||||||
|
lastModifiedAt = Date().time,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package tachiyomi.domain.bookmark.model
|
||||||
|
|
||||||
|
data class BookmarkDelete(
|
||||||
|
val mangaId: Long,
|
||||||
|
val chapterId: Long,
|
||||||
|
val pageIndex: Int? = null,
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package tachiyomi.domain.bookmark.model
|
||||||
|
|
||||||
|
data class BookmarkUpdate(
|
||||||
|
val id: Long,
|
||||||
|
val note: String? = null,
|
||||||
|
)
|
|
@ -0,0 +1,17 @@
|
||||||
|
package tachiyomi.domain.bookmark.model
|
||||||
|
|
||||||
|
data class BookmarkWithChapterNumber(
|
||||||
|
val pageIndex: Int?,
|
||||||
|
val note: String?,
|
||||||
|
val lastModifiedAt: Long,
|
||||||
|
val chapterNumber: Double,
|
||||||
|
) {
|
||||||
|
fun toBookmarkImpl(): Bookmark {
|
||||||
|
return Bookmark.create()
|
||||||
|
.copy(
|
||||||
|
pageIndex = pageIndex,
|
||||||
|
note = note,
|
||||||
|
lastModifiedAt = lastModifiedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package tachiyomi.domain.bookmark.model
|
||||||
|
|
||||||
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single bookmarked page with information
|
||||||
|
* about manga, chapter and bookmark.
|
||||||
|
*/
|
||||||
|
data class BookmarkedPage(
|
||||||
|
val bookmarkId: Long,
|
||||||
|
val mangaId: Long,
|
||||||
|
val chapterId: Long,
|
||||||
|
val pageIndex: Int?,
|
||||||
|
val mangaTitle: String,
|
||||||
|
val chapterNumber: Double,
|
||||||
|
val chapterName: String,
|
||||||
|
val note: String?,
|
||||||
|
val lastModifiedAt: Long,
|
||||||
|
val coverData: MangaCover,
|
||||||
|
)
|
|
@ -0,0 +1,14 @@
|
||||||
|
package tachiyomi.domain.bookmark.model
|
||||||
|
|
||||||
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single Manga with information about number of bookmarks.
|
||||||
|
*/
|
||||||
|
data class MangaWithBookmarks(
|
||||||
|
val mangaId: Long,
|
||||||
|
val mangaTitle: String,
|
||||||
|
val numberOfBookmarks: Long,
|
||||||
|
val bookmarkLastModified: Long,
|
||||||
|
val coverData: MangaCover,
|
||||||
|
)
|
|
@ -0,0 +1,27 @@
|
||||||
|
package tachiyomi.domain.bookmark.repository
|
||||||
|
|
||||||
|
import tachiyomi.domain.bookmark.model.Bookmark
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkDelete
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkUpdate
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkWithChapterNumber
|
||||||
|
import tachiyomi.domain.bookmark.model.BookmarkedPage
|
||||||
|
import tachiyomi.domain.bookmark.model.MangaWithBookmarks
|
||||||
|
|
||||||
|
interface BookmarkRepository {
|
||||||
|
suspend fun get(id: Long): Bookmark?
|
||||||
|
suspend fun get(mangaId: Long, chapterId: Long, pageIndex: Int?): Bookmark?
|
||||||
|
suspend fun getAllByMangaId(mangaId: Long): List<Bookmark>
|
||||||
|
|
||||||
|
suspend fun getMangaWithBookmarks(): List<MangaWithBookmarks>
|
||||||
|
suspend fun getBookmarkedPagesByMangaId(mangaId: Long): List<BookmarkedPage>
|
||||||
|
suspend fun getWithChapterNumberByMangaId(mangaId: Long): List<BookmarkWithChapterNumber>
|
||||||
|
|
||||||
|
suspend fun insert(bookmark: Bookmark)
|
||||||
|
suspend fun updatePartial(update: BookmarkUpdate)
|
||||||
|
suspend fun insertOrReplaceAll(idsToDelete: List<Long>, bookmarksToAdd: List<Bookmark>)
|
||||||
|
|
||||||
|
suspend fun delete(bookmarkId: Long)
|
||||||
|
suspend fun delete(delete: BookmarkDelete)
|
||||||
|
suspend fun deleteAll(delete: List<BookmarkDelete>)
|
||||||
|
suspend fun deleteAllByMangaId(mangaId: Long)
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ data class ChapterUpdate(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val mangaId: Long? = null,
|
val mangaId: Long? = null,
|
||||||
val read: Boolean? = null,
|
val read: Boolean? = null,
|
||||||
val bookmark: Boolean? = null,
|
private var _bookmark: Boolean? = null,
|
||||||
val lastPageRead: Long? = null,
|
val lastPageRead: Long? = null,
|
||||||
val dateFetch: Long? = null,
|
val dateFetch: Long? = null,
|
||||||
val sourceOrder: Long? = null,
|
val sourceOrder: Long? = null,
|
||||||
|
@ -13,7 +13,17 @@ 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 bookmark: Boolean?
|
||||||
|
get() = _bookmark
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Only to be used from set/delete bookmarks components to keep bookmarks record consistent.
|
||||||
|
fun bookmarkUpdate(id: Long, bookmark: Boolean): ChapterUpdate {
|
||||||
|
return ChapterUpdate(id, _bookmark = bookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Chapter.toChapterUpdate(): ChapterUpdate {
|
fun Chapter.toChapterUpdate(): ChapterUpdate {
|
||||||
return ChapterUpdate(
|
return ChapterUpdate(
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<string name="label_backup">Backup and restore</string>
|
<string name="label_backup">Backup and restore</string>
|
||||||
<string name="label_data_storage">Data and storage</string>
|
<string name="label_data_storage">Data and storage</string>
|
||||||
<string name="label_stats">Statistics</string>
|
<string name="label_stats">Statistics</string>
|
||||||
|
<string name="label_bookmarks">Bookmarks</string>
|
||||||
<string name="label_migration">Migrate</string>
|
<string name="label_migration">Migrate</string>
|
||||||
<string name="label_extensions">Extensions</string>
|
<string name="label_extensions">Extensions</string>
|
||||||
<string name="label_extension_info">Extension info</string>
|
<string name="label_extension_info">Extension info</string>
|
||||||
|
@ -84,6 +85,11 @@
|
||||||
<string name="action_download">Download</string>
|
<string name="action_download">Download</string>
|
||||||
<string name="action_bookmark">Bookmark chapter</string>
|
<string name="action_bookmark">Bookmark chapter</string>
|
||||||
<string name="action_remove_bookmark">Unbookmark chapter</string>
|
<string name="action_remove_bookmark">Unbookmark chapter</string>
|
||||||
|
<string name="action_add_page_bookmark">Add page bookmark</string>
|
||||||
|
<string name="action_update_page_bookmark">Update page bookmark</string>
|
||||||
|
<string name="action_update_bookmark">Update bookmark</string>
|
||||||
|
<string name="action_delete_bookmark">Delete bookmark</string>
|
||||||
|
<string name="action_delete_all_bookmarks">Delete all bookmarks</string>
|
||||||
<string name="action_delete">Delete</string>
|
<string name="action_delete">Delete</string>
|
||||||
<string name="action_update_library">Update library</string>
|
<string name="action_update_library">Update library</string>
|
||||||
<string name="action_enable_all">Enable all</string>
|
<string name="action_enable_all">Enable all</string>
|
||||||
|
@ -747,6 +753,7 @@
|
||||||
<!-- Reader activity -->
|
<!-- Reader activity -->
|
||||||
<string name="custom_filter">Custom filter</string>
|
<string name="custom_filter">Custom filter</string>
|
||||||
<string name="set_as_cover">Set as cover</string>
|
<string name="set_as_cover">Set as cover</string>
|
||||||
|
<string name="page_bookmark">Bookmark page</string>
|
||||||
<string name="cover_updated">Cover updated</string>
|
<string name="cover_updated">Cover updated</string>
|
||||||
<string name="share_page_info">%1$s: %2$s, page %3$d</string>
|
<string name="share_page_info">%1$s: %2$s, page %3$d</string>
|
||||||
<string name="chapter_progress">Page: %1$d</string>
|
<string name="chapter_progress">Page: %1$d</string>
|
||||||
|
@ -866,6 +873,7 @@
|
||||||
<!-- Information Text -->
|
<!-- Information Text -->
|
||||||
<string name="information_no_downloads">No downloads</string>
|
<string name="information_no_downloads">No downloads</string>
|
||||||
<string name="information_no_recent">No recent updates</string>
|
<string name="information_no_recent">No recent updates</string>
|
||||||
|
<string name="information_no_bookmarks">No bookmarks</string>
|
||||||
<string name="information_no_recent_manga">Nothing read recently</string>
|
<string name="information_no_recent_manga">Nothing read recently</string>
|
||||||
<string name="information_empty_library">Your library is empty</string>
|
<string name="information_empty_library">Your library is empty</string>
|
||||||
<string name="information_no_manga_category">Category is empty</string>
|
<string name="information_no_manga_category">Category is empty</string>
|
||||||
|
@ -916,4 +924,13 @@
|
||||||
<string name="exception_http">HTTP %d, check website in WebView</string>
|
<string name="exception_http">HTTP %d, check website in WebView</string>
|
||||||
<string name="exception_offline">No Internet connection</string>
|
<string name="exception_offline">No Internet connection</string>
|
||||||
<string name="exception_unknown_host">Couldn\'t reach %s</string>
|
<string name="exception_unknown_host">Couldn\'t reach %s</string>
|
||||||
|
|
||||||
|
<!-- Bookmarks view and editing. -->
|
||||||
|
<string name="bookmark_note_placeholder">Provide an optional bookmark note</string>
|
||||||
|
<string name="bookmark_total_in_manga">Total bookmarks: %1$d</string>
|
||||||
|
<string name="bookmark_last_updated_in_manga">Last bookmark update: %1$s</string>
|
||||||
|
<string name="delete_bookmark_confirmation">Do you wish to delete the bookmark?</string>
|
||||||
|
<string name="bookmark_page_number">Page: %1$d</string>
|
||||||
|
<string name="bookmark_chapter">Chapter bookmark</string>
|
||||||
|
<string name="bookmark_delete_manga_confirmation">Are you sure? Bookmarks will be lost.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Reference in a new issue