diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesDeleteConfirmationDialog.kt similarity index 100% rename from app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt rename to app/src/main/java/eu/kanade/presentation/updates/UpdatesDeleteConfirmationDialog.kt diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt new file mode 100644 index 000000000..3a5362cbc --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt @@ -0,0 +1,111 @@ +package eu.kanade.presentation.updates + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.components.TabbedDialog +import eu.kanade.presentation.components.TabbedDialogPaddings +import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel +import kotlinx.collections.immutable.persistentListOf +import tachiyomi.core.common.preference.getAndSet +import tachiyomi.domain.updates.service.UpdatesPreferences +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.SettingsItemsPaddings +import tachiyomi.presentation.core.components.TriStateItem +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.collectAsState + +@Composable +fun UpdatesFilterDialog( + onDismissRequest: () -> Unit, + screenModel: UpdatesSettingsScreenModel, +) { + TabbedDialog( + onDismissRequest = onDismissRequest, + tabTitles = persistentListOf( + stringResource(MR.strings.action_filter), + ), + ) { + Column( + modifier = Modifier + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()), + ) { + FilterSheet(screenModel = screenModel) + } + } +} + +@Composable +private fun ColumnScope.FilterSheet( + screenModel: UpdatesSettingsScreenModel, +) { + val filterDownloaded by screenModel.updatesPreferences.filterDownloaded().collectAsState() + TriStateItem( + label = stringResource(MR.strings.label_downloaded), + state = filterDownloaded, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) }, + ) + + val filterUnread by screenModel.updatesPreferences.filterUnread().collectAsState() + TriStateItem( + label = stringResource(MR.strings.action_filter_unread), + state = filterUnread, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) }, + ) + + val filterStarted by screenModel.updatesPreferences.filterStarted().collectAsState() + TriStateItem( + label = stringResource(MR.strings.label_started), + state = filterStarted, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) }, + ) + + val filterBookmarked by screenModel.updatesPreferences.filterBookmarked().collectAsState() + TriStateItem( + label = stringResource(MR.strings.action_filter_bookmarked), + state = filterBookmarked, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) }, + ) + + HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small)) + + val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators().collectAsState() + + fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators().getAndSet { !it } + + Row( + modifier = Modifier + .clickable { toggleScanlatorFilter() } + .fillMaxWidth() + .padding(horizontal = SettingsItemsPaddings.Horizontal), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(MR.strings.action_filter_excluded_scanlators), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + ) + + Switch( + checked = filterExcludedScanlators, + onCheckedChange = { toggleScanlatorFilter() }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index e72d229bd..eae69bd99 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -5,9 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CalendarMonth +import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.SelectAll +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.TopAppBarScrollBehavior @@ -37,6 +40,7 @@ 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 +import tachiyomi.presentation.core.theme.active import java.time.LocalDate import kotlin.time.Duration.Companion.seconds @@ -59,6 +63,8 @@ fun UpdateScreen( onMultiDeleteClicked: (List) -> Unit, onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onOpenChapter: (UpdatesItem) -> Unit, + onFilterClicked: () -> Unit, + hasActiveFilters: Boolean, ) { BackHandler(enabled = state.selectionMode) { onSelectAll(false) @@ -69,6 +75,8 @@ fun UpdateScreen( UpdatesAppBar( onCalendarClicked = { onCalendarClicked() }, onUpdateLibrary = { onUpdateLibrary() }, + onFilterClicked = { onFilterClicked() }, + hasFilters = hasActiveFilters, actionModeCounter = state.selected.size, onSelectAll = { onSelectAll(true) }, onInvertSelection = { onInvertSelection() }, @@ -139,6 +147,8 @@ fun UpdateScreen( private fun UpdatesAppBar( onCalendarClicked: () -> Unit, onUpdateLibrary: () -> Unit, + onFilterClicked: () -> Unit, + hasFilters: Boolean, // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, @@ -153,6 +163,12 @@ private fun UpdatesAppBar( actions = { AppBarActions( persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_filter), + icon = Icons.Outlined.FilterList, + iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current, + onClick = onFilterClicked, + ), AppBar.Action( title = stringResource(MR.strings.action_view_upcoming), icon = Icons.Outlined.CalendarMonth, diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt index c482acc47..742664426 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -18,6 +18,7 @@ import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.storage.service.StoragePreferences +import tachiyomi.domain.updates.service.UpdatesPreferences import uy.kohesive.injekt.api.InjektRegistrar class PreferenceModule(val app: Application) : InjektModule { @@ -44,6 +45,9 @@ class PreferenceModule(val app: Application) : InjektModule { addSingletonFactory { LibraryPreferences(get()) } + addSingletonFactory { + UpdatesPreferences(get()) + } addSingletonFactory { ReaderPreferences(get()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 231e14b59..0e1122f56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue +import androidx.compose.ui.util.fastFilter import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.preference.asState @@ -30,11 +31,16 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority +import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.system.logcat @@ -43,9 +49,11 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.GetManga +import tachiyomi.domain.manga.model.applyFilter import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.updates.interactor.GetUpdates import tachiyomi.domain.updates.model.UpdatesWithRelations +import tachiyomi.domain.updates.service.UpdatesPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.ZonedDateTime @@ -60,6 +68,7 @@ class UpdatesScreenModel( private val getManga: GetManga = Injekt.get(), private val getChapter: GetChapter = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), + private val updatesPreferences: UpdatesPreferences = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), // SY --> readerPreferences: ReaderPreferences = Injekt.get(), @@ -85,19 +94,35 @@ class UpdatesScreenModel( val limit = ZonedDateTime.now().minusMonths(3).toInstant() combine( - getUpdates.subscribe(limit).distinctUntilChanged(), + // needed for SQL filters (unread, started, bookmarked, etc) + getUpdatesItemPreferenceFlow() + .distinctUntilChanged() + .flatMapLatest { + getUpdates.subscribe( + limit, + unread = it.filterUnread.toBooleanOrNull(), + started = it.filterStarted.toBooleanOrNull(), + bookmarked = it.filterBookmarked.toBooleanOrNull(), + hideExcludedScanlators = it.filterExcludedScanlators, + ).distinctUntilChanged() + }, downloadCache.changes, downloadManager.queueState, - ) { updates, _, _ -> updates } - .catch { - logcat(LogPriority.ERROR, it) - _events.send(Event.InternalError) - } - .collectLatest { updates -> + // needed for Kotlin filters (downloaded) + getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new -> + old.filterDownloaded == new.filterDownloaded + }, + ) { updates, _, _, itemPreferences -> + updates + .toUpdateItems() + .applyFilters(itemPreferences) + .toPersistentList() + } + .collectLatest { updateItems -> mutableState.update { it.copy( isLoading = false, - items = updates.toUpdateItems(), + items = updateItems, ) } } @@ -108,9 +133,43 @@ class UpdatesScreenModel( .catch { logcat(LogPriority.ERROR, it) } .collect(this@UpdatesScreenModel::updateDownloadState) } + + getUpdatesItemPreferenceFlow() + .map { prefs -> + listOf( + prefs.filterUnread, + prefs.filterDownloaded, + prefs.filterStarted, + prefs.filterBookmarked, + ) + .any { it != TriState.DISABLED } + } + .distinctUntilChanged() + .onEach { + mutableState.update { state -> + state.copy(hasActiveFilters = it) + } + } + .launchIn(screenModelScope) } - private fun List.toUpdateItems(): PersistentList { + private fun List.applyFilters( + preferences: ItemPreferences, + ): List { + val filterDownloaded = preferences.filterDownloaded + + val filterFnDownloaded: (UpdatesItem) -> Boolean = { + applyFilter(filterDownloaded) { + it.downloadStateProvider() == Download.State.DOWNLOADED + } + } + + return fastFilter { + filterFnDownloaded(it) + } + } + + private fun List.toUpdateItems(): List { return this .map { update -> val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId) @@ -135,7 +194,6 @@ class UpdatesScreenModel( selected = update.chapterId in selectedChapterIds, ) } - .toPersistentList() } fun updateLibrary(): Boolean { @@ -373,9 +431,41 @@ class UpdatesScreenModel( libraryPreferences.newUpdatesCount().set(0) } + private fun getUpdatesItemPreferenceFlow(): Flow { + return combine( + updatesPreferences.filterDownloaded().changes(), + updatesPreferences.filterUnread().changes(), + updatesPreferences.filterStarted().changes(), + updatesPreferences.filterBookmarked().changes(), + updatesPreferences.filterExcludedScanlators().changes(), + ) { downloaded, unread, started, bookmarked, excludedScanlators -> + ItemPreferences( + filterDownloaded = downloaded, + filterUnread = unread, + filterStarted = started, + filterBookmarked = bookmarked, + filterExcludedScanlators = excludedScanlators, + ) + } + } + + fun showFilterDialog() { + mutableState.update { it.copy(dialog = Dialog.FilterSheet) } + } + + @Immutable + private data class ItemPreferences( + val filterDownloaded: TriState, + val filterUnread: TriState, + val filterStarted: TriState, + val filterBookmarked: TriState, + val filterExcludedScanlators: Boolean, + ) + @Immutable data class State( val isLoading: Boolean = true, + val hasActiveFilters: Boolean = false, val items: PersistentList = persistentListOf(), val dialog: Dialog? = null, ) { @@ -399,6 +489,7 @@ class UpdatesScreenModel( sealed interface Dialog { data class DeleteConfirmation(val toDelete: List) : Dialog + data object FilterSheet : Dialog } sealed interface Event { @@ -407,6 +498,14 @@ class UpdatesScreenModel( } } +private fun TriState.toBooleanOrNull(): Boolean? { + return when (this) { + TriState.DISABLED -> null + TriState.ENABLED_IS -> true + TriState.ENABLED_NOT -> false + } +} + @Immutable data class UpdatesItem( val update: UpdatesWithRelations, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt new file mode 100644 index 000000000..1e909d484 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.ui.updates + +import cafe.adriel.voyager.core.model.ScreenModel +import tachiyomi.core.common.preference.Preference +import tachiyomi.core.common.preference.TriState +import tachiyomi.core.common.preference.getAndSet +import tachiyomi.domain.updates.service.UpdatesPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class UpdatesSettingsScreenModel( + val updatesPreferences: UpdatesPreferences = Injekt.get(), +) : ScreenModel { + + fun toggleFilter(preference: (UpdatesPreferences) -> Preference) { + preference(updatesPreferences).getAndSet { + it.next() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index 9b55f88d9..0f84a1666 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -21,6 +21,7 @@ import eu.kanade.core.preference.asState import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.updates.UpdateScreen import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog +import eu.kanade.presentation.updates.UpdatesFilterDialog import eu.kanade.presentation.util.Tab import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen @@ -70,6 +71,7 @@ data object UpdatesTab : Tab { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow val screenModel = rememberScreenModel { UpdatesScreenModel() } + val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() } val state by screenModel.state.collectAsState() UpdateScreen( @@ -93,6 +95,8 @@ data object UpdatesTab : Tab { context.startActivity(intent) }, onCalendarClicked = { navigator.push(UpcomingScreen()) }, + onFilterClicked = screenModel::showFilterDialog, + hasActiveFilters = state.hasActiveFilters, ) val onDismissDialog = { screenModel.setDialog(null) } @@ -103,6 +107,12 @@ data object UpdatesTab : Tab { onConfirm = { screenModel.deleteChapters(dialog.toDelete) }, ) } + is UpdatesScreenModel.Dialog.FilterSheet -> { + UpdatesFilterDialog( + onDismissRequest = onDismissDialog, + screenModel = settingsScreenModel, + ) + } null -> {} } diff --git a/data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt b/data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt index f0781c756..8f128335e 100644 --- a/data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt +++ b/data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt @@ -114,6 +114,21 @@ class AndroidDatabaseHandler( // SY --> fun getLibraryQuery(condition: String = "M.favorite = 1") = LibraryQuery(driver, condition) - fun getUpdatesQuery(after: Long, limit: Long) = UpdatesQuery(driver, after, limit) + fun getUpdatesQuery( + after: Long, + limit: Long, + read: Boolean?, + started: Long?, + bookmarked: Boolean?, + hideExcludedScanlators: Long, + ) = UpdatesQuery( + driver, + after, + limit, + read, + started, + bookmarked, + hideExcludedScanlators, + ) // SY <-- } diff --git a/data/src/main/java/tachiyomi/data/UpdatesQuery.kt b/data/src/main/java/tachiyomi/data/UpdatesQuery.kt index 0a729a928..36d381c95 100644 --- a/data/src/main/java/tachiyomi/data/UpdatesQuery.kt +++ b/data/src/main/java/tachiyomi/data/UpdatesQuery.kt @@ -24,72 +24,113 @@ private val mapper = { cursor: SqlCursor -> cursor.getLong(12)!!, cursor.getLong(13)!!, cursor.getLong(14)!!, + cursor.getString(15), ) } -class UpdatesQuery(val driver: SqlDriver, val after: Long, val limit: Long) : ExecutableQuery(mapper) { +class UpdatesQuery( + val driver: SqlDriver, + val after: Long, + val limit: Long, + val read: Boolean?, + val started: Long?, + val bookmarked: Boolean?, + val hideExcludedScanlators: Long, +) : ExecutableQuery(mapper) { override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult { return driver.executeQuery( null, """ - SELECT - mangas._id AS mangaId, - mangas.title AS mangaTitle, - chapters._id AS chapterId, - chapters.name AS chapterName, - chapters.scanlator, - chapters.url AS chapterUrl, - chapters.read, - chapters.bookmark, - chapters.last_page_read, - mangas.source, - mangas.favorite, - mangas.thumbnail_url AS thumbnailUrl, - mangas.cover_last_modified AS coverLastModified, - chapters.date_upload AS dateUpload, - chapters.date_fetch AS datefetch - FROM mangas JOIN chapters - ON mangas._id = chapters.manga_id - WHERE favorite = 1 AND source <> $MERGED_SOURCE_ID - AND date_fetch > date_added - AND dateUpload > :after - UNION - SELECT - mangas._id AS mangaId, - mangas.title AS mangaTitle, - chapters._id AS chapterId, - chapters.name AS chapterName, - chapters.scanlator, - chapters.url AS chapterUrl, - chapters.read, - chapters.bookmark, - chapters.last_page_read, - mangas.source, - mangas.favorite, - mangas.thumbnail_url AS thumbnailUrl, - mangas.cover_last_modified AS coverLastModified, - chapters.date_upload AS dateUpload, - chapters.date_fetch AS datefetch - FROM mangas - LEFT JOIN ( - SELECT merged.manga_id,merged.merge_id - FROM merged - GROUP BY merged.merge_id - ) as ME - ON ME.merge_id = mangas._id - JOIN chapters - ON ME.manga_id = chapters.manga_id - WHERE favorite = 1 AND source = $MERGED_SOURCE_ID - AND date_fetch > date_added - AND dateUpload > :after - ORDER BY datefetch DESC + SELECT * + FROM ( + -- Normal source + SELECT + mangas._id AS mangaId, + mangas.title AS mangaTitle, + chapters._id AS chapterId, + chapters.name AS chapterName, + chapters.scanlator, + chapters.url AS chapterUrl, + chapters.read, + chapters.bookmark, + chapters.last_page_read, + mangas.source, + mangas.favorite, + mangas.thumbnail_url AS thumbnailUrl, + mangas.cover_last_modified AS coverLastModified, + chapters.date_upload AS dateUpload, + chapters.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator + FROM mangas + JOIN chapters + ON mangas._id = chapters.manga_id + LEFT JOIN excluded_scanlators + ON mangas._id = excluded_scanlators.manga_id + AND chapters.scanlator = excluded_scanlators.scanlator + WHERE mangas.source <> $MERGED_SOURCE_ID + AND date_fetch > date_added + + UNION ALL + + -- Merged source + SELECT + mangas._id AS mangaId, + mangas.title AS mangaTitle, + chapters._id AS chapterId, + chapters.name AS chapterName, + chapters.scanlator, + chapters.url AS chapterUrl, + chapters.read, + chapters.bookmark, + chapters.last_page_read, + mangas.source, + mangas.favorite, + mangas.thumbnail_url AS thumbnailUrl, + mangas.cover_last_modified AS coverLastModified, + chapters.date_upload AS dateUpload, + chapters.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator + FROM mangas + LEFT JOIN ( + SELECT merged.manga_id, merged.merge_id + FROM merged + GROUP BY merged.merge_id + ) AS ME + ON ME.merge_id = mangas._id + JOIN chapters + ON ME.manga_id = chapters.manga_id + LEFT JOIN excluded_scanlators + ON ME.merge_id = excluded_scanlators.manga_id + AND chapters.scanlator = excluded_scanlators.scanlator + WHERE mangas.source = $MERGED_SOURCE_ID + AND date_fetch > date_added + ) AS combined + WHERE + favorite = 1 + AND dateUpload > :after + AND (:read IS NULL OR read = :read) + AND ( + :started IS NULL + OR (:started = 1 AND last_page_read > 0 AND read = 0) + OR (:started = 0 AND last_page_read = 0 AND read = 0) + ) + AND (:bookmarked IS NULL OR bookmark = :bookmarked) + AND ( + excludedScanlator IS NULL OR :hideExcludedScanlators = 0 + ) + ORDER BY datefetch DESC LIMIT :limit; """.trimIndent(), mapper, - 2, + 6, binders = { - bindLong(0, after) - bindLong(1, limit) + var parameterIndex = 0 + bindLong(parameterIndex++, after) + bindBoolean(parameterIndex++, read) + bindLong(parameterIndex++, started) + bindBoolean(parameterIndex++, bookmarked) + bindLong(parameterIndex++, hideExcludedScanlators) + bindLong(parameterIndex++, limit) }, ) } diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt index 303597736..f5d7b1499 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt @@ -2,6 +2,7 @@ package tachiyomi.data.updates import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import tachiyomi.core.common.util.lang.toLong import tachiyomi.data.AndroidDatabaseHandler import tachiyomi.data.DatabaseHandler import tachiyomi.domain.manga.model.MangaCover @@ -28,12 +29,36 @@ class UpdatesRepositoryImpl( } } - override fun subscribeAll(after: Long, limit: Long): Flow> { + override fun subscribeAll( + after: Long, + limit: Long, + unread: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> { return databaseHandler.subscribeToList { - updatesViewQueries.getRecentUpdates(after, limit, ::mapUpdatesWithRelations) + updatesViewQueries.getRecentUpdatesWithFilters( + after = after, + limit = limit, + // invert because unread in Kotlin -> read column in SQL + read = unread?.let { !it }, + started = started?.toLong(), + bookmarked = bookmarked, + hideExcludedScanlators = hideExcludedScanlators.toLong(), + mapper = ::mapUpdatesWithRelations, + ) }.map { databaseHandler.awaitListExecutable { - (databaseHandler as AndroidDatabaseHandler).getUpdatesQuery(after, limit) + (databaseHandler as AndroidDatabaseHandler).getUpdatesQuery( + after = after, + limit = limit, + // invert because unread in Kotlin -> read column in SQL + read = unread?.let { !it }, + started = started?.toLong(), + bookmarked = bookmarked, + hideExcludedScanlators = hideExcludedScanlators.toLong(), + ) } .map(::mapUpdatesView) } @@ -70,6 +95,7 @@ class UpdatesRepositoryImpl( coverLastModified: Long, dateUpload: Long, dateFetch: Long, + excludedScanlator: String?, ): UpdatesWithRelations = UpdatesWithRelations( mangaId = mangaId, // SY --> diff --git a/data/src/main/sqldelight/tachiyomi/migrations/38.sqm b/data/src/main/sqldelight/tachiyomi/migrations/38.sqm new file mode 100644 index 000000000..0ad345f75 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/38.sqm @@ -0,0 +1,29 @@ +-- Add excluded_scanlators to updatesView +DROP VIEW IF EXISTS updatesView; + +CREATE VIEW updatesView AS +SELECT + mangas._id AS mangaId, + mangas.title AS mangaTitle, + chapters._id AS chapterId, + chapters.name AS chapterName, + chapters.scanlator, + chapters.url AS chapterUrl, + chapters.read, + chapters.bookmark, + chapters.last_page_read, + mangas.source, + mangas.favorite, + mangas.thumbnail_url AS thumbnailUrl, + mangas.cover_last_modified AS coverLastModified, + chapters.date_upload AS dateUpload, + chapters.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator +FROM mangas JOIN chapters +ON mangas._id = chapters.manga_id +LEFT JOIN excluded_scanlators +ON mangas._id = excluded_scanlators.manga_id +AND chapters.scanlator = excluded_scanlators.scanlator +WHERE favorite = 1 +AND date_fetch > date_added +ORDER BY date_fetch DESC; diff --git a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq index f18c55401..cfe904cf1 100644 --- a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq @@ -14,9 +14,13 @@ SELECT mangas.thumbnail_url AS thumbnailUrl, mangas.cover_last_modified AS coverLastModified, chapters.date_upload AS dateUpload, - chapters.date_fetch AS datefetch + chapters.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator FROM mangas JOIN chapters ON mangas._id = chapters.manga_id +LEFT JOIN excluded_scanlators +ON mangas._id = excluded_scanlators.manga_id +AND chapters.scanlator = excluded_scanlators.scanlator WHERE favorite = 1 AND date_fetch > date_added ORDER BY date_fetch DESC; @@ -27,6 +31,23 @@ FROM updatesView WHERE dateUpload > :after LIMIT :limit; +getRecentUpdatesWithFilters: +SELECT * +FROM updatesView +WHERE dateUpload > :after +AND (:read IS NULL OR read = :read) +-- Started means some progress but not finished, Read means finished chapter, thus: +AND ( + :started IS NULL + OR (:started = 1 AND last_page_read > 0 AND read = 0) + OR (:started = 0 AND last_page_read = 0 AND read = 0) +) +AND (:bookmarked IS NULL OR bookmark = :bookmarked) +AND ( + (excludedScanlator IS NULL OR :hideExcludedScanlators = 0) +) +LIMIT :limit; + getUpdatesByReadStatus: SELECT * FROM updatesView diff --git a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt index 2fc098e55..018220d3f 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt @@ -24,8 +24,21 @@ class GetUpdates( // SY <-- } - fun subscribe(instant: Instant): Flow> { - return repository.subscribeAll(instant.toEpochMilli(), limit = 500) + fun subscribe( + instant: Instant, + unread: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> { + return repository.subscribeAll( + instant.toEpochMilli(), + limit = 500, + unread = unread, + started = started, + bookmarked = bookmarked, + hideExcludedScanlators = hideExcludedScanlators, + ) // SY --> .catchNPE() // SY <-- diff --git a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt index 3b583ea90..ca738f92d 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt @@ -7,7 +7,14 @@ interface UpdatesRepository { suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List - fun subscribeAll(after: Long, limit: Long): Flow> + fun subscribeAll( + after: Long, + limit: Long, + unread: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow> } diff --git a/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt b/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt new file mode 100644 index 000000000..fa86de29b --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt @@ -0,0 +1,35 @@ +package tachiyomi.domain.updates.service + +import tachiyomi.core.common.preference.PreferenceStore +import tachiyomi.core.common.preference.TriState +import tachiyomi.core.common.preference.getEnum + +class UpdatesPreferences( + private val preferenceStore: PreferenceStore, +) { + + fun filterDownloaded() = preferenceStore.getEnum( + "pref_filter_updates_downloaded", + TriState.DISABLED, + ) + + fun filterUnread() = preferenceStore.getEnum( + "pref_filter_updates_unread", + TriState.DISABLED, + ) + + fun filterStarted() = preferenceStore.getEnum( + "pref_filter_updates_started", + TriState.DISABLED, + ) + + fun filterBookmarked() = preferenceStore.getEnum( + "pref_filter_updates_bookmarked", + TriState.DISABLED, + ) + + fun filterExcludedScanlators() = preferenceStore.getBoolean( + "pref_filter_updates_hide_excluded_scanlators", + false, + ) +} diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index e5725cb3e..1b91052cb 100755 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -855,6 +855,7 @@ Just now Never View Upcoming Updates + Filter excluded scanlators Upcoming Guide