Add Filters to Updates screen (#2851)

* Add Filters to Updates screen

Behaves basically like the filters in the library:

- Unread: Show/Don't show unread chapters
- Downloaded: Show/Don't show downloaded chapters
- Started: Show/Don't show chapters that have some progress but aren't
  fully Read
- Bookmarked: Show/Don't show chapters that have been bookmarked

Started behaves differently from its Library counterpart because the
actual manga data is not available at this point in time and I thought
calling getManga for each entry without caching would be a pretty bad
idea.

I have modelled this closely on the filter control flow in the
Library, but I'm sure this can be simplified/adjusted in some way.

* Move most filtering logic to SQL

Unread, Started, and Bookmarked filters are now part of the SQL query.

Download state cannot be filtered in the database so it remains in
Kotlin.

Because the Downloaded filter has to be run in Kotlin, the combine
flow uses the preferences flow twice, once to get the SQL query params
and once for the Kotlin filters (only Downloaded at this time).

* Add "Hide excluded scanlators" to update filters

Based on the work done in #1623 but integrated with the other filters
in this PR. Added the user as a co-author for credit.

Co-authored-by: Dani <17619547+shabnix@users.noreply.github.com>

---------

Co-authored-by: Dani <17619547+shabnix@users.noreply.github.com>
(cherry picked from commit bbe9aa8561360f030072fbc49f79748e71c6535e)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt
#	data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt
#	data/src/main/sqldelight/tachiyomi/migrations/9.sqm
#	domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt
This commit is contained in:
MajorTanya
2026-01-17 11:43:40 +01:00
committed by Jobobby04
parent b0d6e16ca3
commit 1f51569a35
16 changed files with 521 additions and 73 deletions
@@ -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() },
)
}
}
@@ -5,9 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
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.CalendarMonth 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.FlipToBack
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.SelectAll 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.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarScrollBehavior 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.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.active
import java.time.LocalDate import java.time.LocalDate
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -59,6 +63,8 @@ fun UpdateScreen(
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit, onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
onOpenChapter: (UpdatesItem) -> Unit, onOpenChapter: (UpdatesItem) -> Unit,
onFilterClicked: () -> Unit,
hasActiveFilters: Boolean,
) { ) {
BackHandler(enabled = state.selectionMode) { BackHandler(enabled = state.selectionMode) {
onSelectAll(false) onSelectAll(false)
@@ -69,6 +75,8 @@ fun UpdateScreen(
UpdatesAppBar( UpdatesAppBar(
onCalendarClicked = { onCalendarClicked() }, onCalendarClicked = { onCalendarClicked() },
onUpdateLibrary = { onUpdateLibrary() }, onUpdateLibrary = { onUpdateLibrary() },
onFilterClicked = { onFilterClicked() },
hasFilters = hasActiveFilters,
actionModeCounter = state.selected.size, actionModeCounter = state.selected.size,
onSelectAll = { onSelectAll(true) }, onSelectAll = { onSelectAll(true) },
onInvertSelection = { onInvertSelection() }, onInvertSelection = { onInvertSelection() },
@@ -139,6 +147,8 @@ fun UpdateScreen(
private fun UpdatesAppBar( private fun UpdatesAppBar(
onCalendarClicked: () -> Unit, onCalendarClicked: () -> Unit,
onUpdateLibrary: () -> Unit, onUpdateLibrary: () -> Unit,
onFilterClicked: () -> Unit,
hasFilters: Boolean,
// For action mode // For action mode
actionModeCounter: Int, actionModeCounter: Int,
onSelectAll: () -> Unit, onSelectAll: () -> Unit,
@@ -153,6 +163,12 @@ private fun UpdatesAppBar(
actions = { actions = {
AppBarActions( AppBarActions(
persistentListOf( 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( AppBar.Action(
title = stringResource(MR.strings.action_view_upcoming), title = stringResource(MR.strings.action_view_upcoming),
icon = Icons.Outlined.CalendarMonth, icon = Icons.Outlined.CalendarMonth,
@@ -18,6 +18,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
class PreferenceModule(val app: Application) : InjektModule { class PreferenceModule(val app: Application) : InjektModule {
@@ -44,6 +45,9 @@ class PreferenceModule(val app: Application) : InjektModule {
addSingletonFactory { addSingletonFactory {
LibraryPreferences(get()) LibraryPreferences(get())
} }
addSingletonFactory {
UpdatesPreferences(get())
}
addSingletonFactory { addSingletonFactory {
ReaderPreferences(get()) ReaderPreferences(get())
} }
@@ -4,6 +4,7 @@ import android.app.Application
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.util.fastFilter
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
@@ -30,11 +31,16 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged 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.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.system.logcat 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.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.manga.model.applyFilter
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.updates.interactor.GetUpdates import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.model.UpdatesWithRelations import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime import java.time.ZonedDateTime
@@ -60,6 +68,7 @@ class UpdatesScreenModel(
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 updatesPreferences: UpdatesPreferences = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
// SY --> // SY -->
readerPreferences: ReaderPreferences = Injekt.get(), readerPreferences: ReaderPreferences = Injekt.get(),
@@ -85,19 +94,35 @@ class UpdatesScreenModel(
val limit = ZonedDateTime.now().minusMonths(3).toInstant() val limit = ZonedDateTime.now().minusMonths(3).toInstant()
combine( 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, downloadCache.changes,
downloadManager.queueState, downloadManager.queueState,
) { updates, _, _ -> updates } // needed for Kotlin filters (downloaded)
.catch { getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new ->
logcat(LogPriority.ERROR, it) old.filterDownloaded == new.filterDownloaded
_events.send(Event.InternalError) },
) { updates, _, _, itemPreferences ->
updates
.toUpdateItems()
.applyFilters(itemPreferences)
.toPersistentList()
} }
.collectLatest { updates -> .collectLatest { updateItems ->
mutableState.update { mutableState.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
items = updates.toUpdateItems(), items = updateItems,
) )
} }
} }
@@ -108,9 +133,43 @@ class UpdatesScreenModel(
.catch { logcat(LogPriority.ERROR, it) } .catch { logcat(LogPriority.ERROR, it) }
.collect(this@UpdatesScreenModel::updateDownloadState) .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<UpdatesWithRelations>.toUpdateItems(): PersistentList<UpdatesItem> { private fun List<UpdatesItem>.applyFilters(
preferences: ItemPreferences,
): List<UpdatesItem> {
val filterDownloaded = preferences.filterDownloaded
val filterFnDownloaded: (UpdatesItem) -> Boolean = {
applyFilter(filterDownloaded) {
it.downloadStateProvider() == Download.State.DOWNLOADED
}
}
return fastFilter {
filterFnDownloaded(it)
}
}
private fun List<UpdatesWithRelations>.toUpdateItems(): List<UpdatesItem> {
return this return this
.map { update -> .map { update ->
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId) val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
@@ -135,7 +194,6 @@ class UpdatesScreenModel(
selected = update.chapterId in selectedChapterIds, selected = update.chapterId in selectedChapterIds,
) )
} }
.toPersistentList()
} }
fun updateLibrary(): Boolean { fun updateLibrary(): Boolean {
@@ -373,9 +431,41 @@ class UpdatesScreenModel(
libraryPreferences.newUpdatesCount().set(0) libraryPreferences.newUpdatesCount().set(0)
} }
private fun getUpdatesItemPreferenceFlow(): Flow<ItemPreferences> {
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 @Immutable
data class State( data class State(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val hasActiveFilters: Boolean = false,
val items: PersistentList<UpdatesItem> = persistentListOf(), val items: PersistentList<UpdatesItem> = persistentListOf(),
val dialog: Dialog? = null, val dialog: Dialog? = null,
) { ) {
@@ -399,6 +489,7 @@ class UpdatesScreenModel(
sealed interface Dialog { sealed interface Dialog {
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
data object FilterSheet : Dialog
} }
sealed interface Event { 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 @Immutable
data class UpdatesItem( data class UpdatesItem(
val update: UpdatesWithRelations, val update: UpdatesWithRelations,
@@ -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<TriState>) {
preference(updatesPreferences).getAndSet {
it.next()
}
}
}
@@ -21,6 +21,7 @@ import eu.kanade.core.preference.asState
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.updates.UpdateScreen import eu.kanade.presentation.updates.UpdateScreen
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
import eu.kanade.presentation.updates.UpdatesFilterDialog
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.ui.download.DownloadQueueScreen import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
@@ -70,6 +71,7 @@ data object UpdatesTab : Tab {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { UpdatesScreenModel() } val screenModel = rememberScreenModel { UpdatesScreenModel() }
val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
UpdateScreen( UpdateScreen(
@@ -93,6 +95,8 @@ data object UpdatesTab : Tab {
context.startActivity(intent) context.startActivity(intent)
}, },
onCalendarClicked = { navigator.push(UpcomingScreen()) }, onCalendarClicked = { navigator.push(UpcomingScreen()) },
onFilterClicked = screenModel::showFilterDialog,
hasActiveFilters = state.hasActiveFilters,
) )
val onDismissDialog = { screenModel.setDialog(null) } val onDismissDialog = { screenModel.setDialog(null) }
@@ -103,6 +107,12 @@ data object UpdatesTab : Tab {
onConfirm = { screenModel.deleteChapters(dialog.toDelete) }, onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
) )
} }
is UpdatesScreenModel.Dialog.FilterSheet -> {
UpdatesFilterDialog(
onDismissRequest = onDismissDialog,
screenModel = settingsScreenModel,
)
}
null -> {} null -> {}
} }
@@ -114,6 +114,21 @@ class AndroidDatabaseHandler(
// SY --> // SY -->
fun getLibraryQuery(condition: String = "M.favorite = 1") = LibraryQuery(driver, condition) 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 <-- // SY <--
} }
@@ -24,14 +24,26 @@ private val mapper = { cursor: SqlCursor ->
cursor.getLong(12)!!, cursor.getLong(12)!!,
cursor.getLong(13)!!, cursor.getLong(13)!!,
cursor.getLong(14)!!, cursor.getLong(14)!!,
cursor.getString(15),
) )
} }
class UpdatesQuery(val driver: SqlDriver, val after: Long, val limit: Long) : ExecutableQuery<UpdatesView>(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<UpdatesView>(mapper) {
override fun <R> execute(mapper: (SqlCursor) -> QueryResult<R>): QueryResult<R> { override fun <R> execute(mapper: (SqlCursor) -> QueryResult<R>): QueryResult<R> {
return driver.executeQuery( return driver.executeQuery(
null, null,
""" """
SELECT *
FROM (
-- Normal source
SELECT SELECT
mangas._id AS mangaId, mangas._id AS mangaId,
mangas.title AS mangaTitle, mangas.title AS mangaTitle,
@@ -47,13 +59,20 @@ class UpdatesQuery(val driver: SqlDriver, val after: Long, val limit: Long) : Ex
mangas.thumbnail_url AS thumbnailUrl, mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified, mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload, chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch chapters.date_fetch AS datefetch,
FROM mangas JOIN chapters excluded_scanlators.scanlator AS excludedScanlator
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id ON mangas._id = chapters.manga_id
WHERE favorite = 1 AND source <> $MERGED_SOURCE_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 AND date_fetch > date_added
AND dateUpload > :after
UNION UNION ALL
-- Merged source
SELECT SELECT
mangas._id AS mangaId, mangas._id AS mangaId,
mangas.title AS mangaTitle, mangas.title AS mangaTitle,
@@ -69,27 +88,49 @@ class UpdatesQuery(val driver: SqlDriver, val after: Long, val limit: Long) : Ex
mangas.thumbnail_url AS thumbnailUrl, mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified, mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload, chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch chapters.date_fetch AS datefetch,
excluded_scanlators.scanlator AS excludedScanlator
FROM mangas FROM mangas
LEFT JOIN ( LEFT JOIN (
SELECT merged.manga_id, merged.merge_id SELECT merged.manga_id, merged.merge_id
FROM merged FROM merged
GROUP BY merged.merge_id GROUP BY merged.merge_id
) as ME ) AS ME
ON ME.merge_id = mangas._id ON ME.merge_id = mangas._id
JOIN chapters JOIN chapters
ON ME.manga_id = chapters.manga_id ON ME.manga_id = chapters.manga_id
WHERE favorite = 1 AND source = $MERGED_SOURCE_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 AND date_fetch > date_added
) AS combined
WHERE
favorite = 1
AND dateUpload > :after 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 ORDER BY datefetch DESC
LIMIT :limit; LIMIT :limit;
""".trimIndent(), """.trimIndent(),
mapper, mapper,
2, 6,
binders = { binders = {
bindLong(0, after) var parameterIndex = 0
bindLong(1, limit) bindLong(parameterIndex++, after)
bindBoolean(parameterIndex++, read)
bindLong(parameterIndex++, started)
bindBoolean(parameterIndex++, bookmarked)
bindLong(parameterIndex++, hideExcludedScanlators)
bindLong(parameterIndex++, limit)
}, },
) )
} }
@@ -2,6 +2,7 @@ package tachiyomi.data.updates
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import tachiyomi.core.common.util.lang.toLong
import tachiyomi.data.AndroidDatabaseHandler import tachiyomi.data.AndroidDatabaseHandler
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
@@ -28,12 +29,36 @@ class UpdatesRepositoryImpl(
} }
} }
override fun subscribeAll(after: Long, limit: Long): Flow<List<UpdatesWithRelations>> { override fun subscribeAll(
after: Long,
limit: Long,
unread: Boolean?,
started: Boolean?,
bookmarked: Boolean?,
hideExcludedScanlators: Boolean,
): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList { 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 { }.map {
databaseHandler.awaitListExecutable { 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) .map(::mapUpdatesView)
} }
@@ -70,6 +95,7 @@ class UpdatesRepositoryImpl(
coverLastModified: Long, coverLastModified: Long,
dateUpload: Long, dateUpload: Long,
dateFetch: Long, dateFetch: Long,
excludedScanlator: String?,
): UpdatesWithRelations = UpdatesWithRelations( ): UpdatesWithRelations = UpdatesWithRelations(
mangaId = mangaId, mangaId = mangaId,
// SY --> // SY -->
@@ -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;
@@ -14,9 +14,13 @@ SELECT
mangas.thumbnail_url AS thumbnailUrl, mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified, mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload, chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch chapters.date_fetch AS datefetch,
excluded_scanlators.scanlator AS excludedScanlator
FROM mangas JOIN chapters FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id 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 WHERE favorite = 1
AND date_fetch > date_added AND date_fetch > date_added
ORDER BY date_fetch DESC; ORDER BY date_fetch DESC;
@@ -27,6 +31,23 @@ FROM updatesView
WHERE dateUpload > :after WHERE dateUpload > :after
LIMIT :limit; 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: getUpdatesByReadStatus:
SELECT * SELECT *
FROM updatesView FROM updatesView
@@ -24,8 +24,21 @@ class GetUpdates(
// SY <-- // SY <--
} }
fun subscribe(instant: Instant): Flow<List<UpdatesWithRelations>> { fun subscribe(
return repository.subscribeAll(instant.toEpochMilli(), limit = 500) instant: Instant,
unread: Boolean?,
started: Boolean?,
bookmarked: Boolean?,
hideExcludedScanlators: Boolean,
): Flow<List<UpdatesWithRelations>> {
return repository.subscribeAll(
instant.toEpochMilli(),
limit = 500,
unread = unread,
started = started,
bookmarked = bookmarked,
hideExcludedScanlators = hideExcludedScanlators,
)
// SY --> // SY -->
.catchNPE() .catchNPE()
// SY <-- // SY <--
@@ -7,7 +7,14 @@ interface UpdatesRepository {
suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List<UpdatesWithRelations> suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List<UpdatesWithRelations>
fun subscribeAll(after: Long, limit: Long): Flow<List<UpdatesWithRelations>> fun subscribeAll(
after: Long,
limit: Long,
unread: Boolean?,
started: Boolean?,
bookmarked: Boolean?,
hideExcludedScanlators: Boolean,
): Flow<List<UpdatesWithRelations>>
fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>> fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
} }
@@ -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,
)
}
@@ -855,6 +855,7 @@
<string name="updates_last_update_info_just_now">Just now</string> <string name="updates_last_update_info_just_now">Just now</string>
<string name="relative_time_span_never">Never</string> <string name="relative_time_span_never">Never</string>
<string name="action_view_upcoming">View Upcoming Updates</string> <string name="action_view_upcoming">View Upcoming Updates</string>
<string name="action_filter_excluded_scanlators">Filter excluded scanlators</string>
<!-- Upcoming --> <!-- Upcoming -->
<string name="upcoming_guide">Upcoming Guide</string> <string name="upcoming_guide">Upcoming Guide</string>