mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Implement scanlator filter (#8803)
* Implement scanlator filter * Visual improvement to scanlator filter dialog * Review changes + Bug fixes Backup not containing filtered chapters and similar issue fix * Review Changes + Fix SQL query * Lint mamma mia
This commit is contained in:
		@@ -22,7 +22,7 @@ android {
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
        applicationId = "eu.kanade.tachiyomi"
 | 
			
		||||
 | 
			
		||||
        versionCode = 108
 | 
			
		||||
        versionCode = 109
 | 
			
		||||
        versionName = "0.14.7"
 | 
			
		||||
 | 
			
		||||
        buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,14 @@
 | 
			
		||||
package eu.kanade.domain
 | 
			
		||||
 | 
			
		||||
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
 | 
			
		||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
 | 
			
		||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
 | 
			
		||||
import eu.kanade.domain.download.interactor.DeleteDownload
 | 
			
		||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
 | 
			
		||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
 | 
			
		||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
 | 
			
		||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
 | 
			
		||||
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
 | 
			
		||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
 | 
			
		||||
import eu.kanade.domain.manga.interactor.UpdateManga
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetEnabledSources
 | 
			
		||||
@@ -112,6 +115,8 @@ class DomainModule : InjektModule {
 | 
			
		||||
        addFactory { NetworkToLocalManga(get()) }
 | 
			
		||||
        addFactory { UpdateManga(get(), get()) }
 | 
			
		||||
        addFactory { SetMangaCategories(get()) }
 | 
			
		||||
        addFactory { GetExcludedScanlators(get()) }
 | 
			
		||||
        addFactory { SetExcludedScanlators(get()) }
 | 
			
		||||
 | 
			
		||||
        addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
 | 
			
		||||
        addFactory { GetApplicationRelease(get(), get()) }
 | 
			
		||||
@@ -133,7 +138,8 @@ class DomainModule : InjektModule {
 | 
			
		||||
        addFactory { UpdateChapter(get()) }
 | 
			
		||||
        addFactory { SetReadStatus(get(), get(), get(), get()) }
 | 
			
		||||
        addFactory { ShouldUpdateDbChapter() }
 | 
			
		||||
        addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
 | 
			
		||||
        addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
 | 
			
		||||
        addFactory { GetAvailableScanlators(get()) }
 | 
			
		||||
 | 
			
		||||
        addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
 | 
			
		||||
        addFactory { GetHistory(get()) }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
package eu.kanade.domain.chapter.interactor
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import tachiyomi.domain.chapter.repository.ChapterRepository
 | 
			
		||||
 | 
			
		||||
class GetAvailableScanlators(
 | 
			
		||||
    private val repository: ChapterRepository,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    private fun List<String>.cleanupAvailableScanlators(): Set<String> {
 | 
			
		||||
        return mapNotNull { it.ifBlank { null } }.toSet()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun await(mangaId: Long): Set<String> {
 | 
			
		||||
        return repository.getScanlatorsByMangaId(mangaId)
 | 
			
		||||
            .cleanupAvailableScanlators()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun subscribe(mangaId: Long): Flow<Set<String>> {
 | 
			
		||||
        return repository.getScanlatorsByMangaIdAsFlow(mangaId)
 | 
			
		||||
            .map { it.cleanupAvailableScanlators() }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,6 +2,7 @@ package eu.kanade.domain.chapter.interactor
 | 
			
		||||
 | 
			
		||||
import eu.kanade.domain.chapter.model.copyFromSChapter
 | 
			
		||||
import eu.kanade.domain.chapter.model.toSChapter
 | 
			
		||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
 | 
			
		||||
import eu.kanade.domain.manga.interactor.UpdateManga
 | 
			
		||||
import eu.kanade.domain.manga.model.toSManga
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
@@ -33,6 +34,7 @@ class SyncChaptersWithSource(
 | 
			
		||||
    private val updateManga: UpdateManga,
 | 
			
		||||
    private val updateChapter: UpdateChapter,
 | 
			
		||||
    private val getChaptersByMangaId: GetChaptersByMangaId,
 | 
			
		||||
    private val getExcludedScanlators: GetExcludedScanlators,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -208,6 +210,10 @@ class SyncChaptersWithSource(
 | 
			
		||||
 | 
			
		||||
        val reAddedUrls = reAdded.map { it.url }.toHashSet()
 | 
			
		||||
 | 
			
		||||
        return updatedToAdd.filterNot { it.url in reAddedUrls }
 | 
			
		||||
        val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
 | 
			
		||||
 | 
			
		||||
        return updatedToAdd.filterNot {
 | 
			
		||||
            it.url in reAddedUrls || it.scanlator in excludedScanlators
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
package eu.kanade.domain.manga.interactor
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import tachiyomi.data.DatabaseHandler
 | 
			
		||||
 | 
			
		||||
class GetExcludedScanlators(
 | 
			
		||||
    private val handler: DatabaseHandler,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    suspend fun await(mangaId: Long): Set<String> {
 | 
			
		||||
        return handler.awaitList {
 | 
			
		||||
            excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
 | 
			
		||||
        }
 | 
			
		||||
            .toSet()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun subscribe(mangaId: Long): Flow<Set<String>> {
 | 
			
		||||
        return handler.subscribeToList {
 | 
			
		||||
            excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
 | 
			
		||||
        }
 | 
			
		||||
            .map { it.toSet() }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package eu.kanade.domain.manga.interactor
 | 
			
		||||
 | 
			
		||||
import tachiyomi.data.DatabaseHandler
 | 
			
		||||
 | 
			
		||||
class SetExcludedScanlators(
 | 
			
		||||
    private val handler: DatabaseHandler,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
 | 
			
		||||
        handler.await(inTransaction = true) {
 | 
			
		||||
            val currentExcluded = handler.awaitList {
 | 
			
		||||
                excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
 | 
			
		||||
            }.toSet()
 | 
			
		||||
            val toAdd = excludedScanlators.minus(currentExcluded)
 | 
			
		||||
            for (scanlator in toAdd) {
 | 
			
		||||
                excluded_scanlatorsQueries.insert(mangaId, scanlator)
 | 
			
		||||
            }
 | 
			
		||||
            val toRemove = currentExcluded.minus(excludedScanlators)
 | 
			
		||||
            excluded_scanlatorsQueries.remove(mangaId, toRemove)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +1,21 @@
 | 
			
		||||
package eu.kanade.presentation.manga
 | 
			
		||||
 | 
			
		||||
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.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.outlined.PeopleAlt
 | 
			
		||||
import androidx.compose.material3.AlertDialog
 | 
			
		||||
import androidx.compose.material3.DropdownMenuItem
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.LocalContentColor
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.material3.TextButton
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
@@ -15,6 +23,7 @@ import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.saveable.rememberSaveable
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
@@ -29,6 +38,7 @@ import tachiyomi.presentation.core.components.LabeledCheckbox
 | 
			
		||||
import tachiyomi.presentation.core.components.RadioItem
 | 
			
		||||
import tachiyomi.presentation.core.components.SortItem
 | 
			
		||||
import tachiyomi.presentation.core.components.TriStateItem
 | 
			
		||||
import tachiyomi.presentation.core.theme.active
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun ChapterSettingsDialog(
 | 
			
		||||
@@ -37,6 +47,8 @@ fun ChapterSettingsDialog(
 | 
			
		||||
    onDownloadFilterChanged: (TriState) -> Unit,
 | 
			
		||||
    onUnreadFilterChanged: (TriState) -> Unit,
 | 
			
		||||
    onBookmarkedFilterChanged: (TriState) -> Unit,
 | 
			
		||||
    scanlatorFilterActive: Boolean,
 | 
			
		||||
    onScanlatorFilterClicked: (() -> Unit),
 | 
			
		||||
    onSortModeChanged: (Long) -> Unit,
 | 
			
		||||
    onDisplayModeChanged: (Long) -> Unit,
 | 
			
		||||
    onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
 | 
			
		||||
@@ -89,6 +101,8 @@ fun ChapterSettingsDialog(
 | 
			
		||||
                        onUnreadFilterChanged = onUnreadFilterChanged,
 | 
			
		||||
                        bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
 | 
			
		||||
                        onBookmarkedFilterChanged = onBookmarkedFilterChanged,
 | 
			
		||||
                        scanlatorFilterActive = scanlatorFilterActive,
 | 
			
		||||
                        onScanlatorFilterClicked = onScanlatorFilterClicked,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                1 -> {
 | 
			
		||||
@@ -117,6 +131,8 @@ private fun ColumnScope.FilterPage(
 | 
			
		||||
    onUnreadFilterChanged: (TriState) -> Unit,
 | 
			
		||||
    bookmarkedFilter: TriState,
 | 
			
		||||
    onBookmarkedFilterChanged: (TriState) -> Unit,
 | 
			
		||||
    scanlatorFilterActive: Boolean,
 | 
			
		||||
    onScanlatorFilterClicked: (() -> Unit),
 | 
			
		||||
) {
 | 
			
		||||
    TriStateItem(
 | 
			
		||||
        label = stringResource(R.string.label_downloaded),
 | 
			
		||||
@@ -133,6 +149,39 @@ private fun ColumnScope.FilterPage(
 | 
			
		||||
        state = bookmarkedFilter,
 | 
			
		||||
        onClick = onBookmarkedFilterChanged,
 | 
			
		||||
    )
 | 
			
		||||
    ScanlatorFilterItem(
 | 
			
		||||
        active = scanlatorFilterActive,
 | 
			
		||||
        onClick = onScanlatorFilterClicked,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun ScanlatorFilterItem(
 | 
			
		||||
    active: Boolean,
 | 
			
		||||
    onClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Row(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .clickable(onClick = onClick)
 | 
			
		||||
            .fillMaxWidth()
 | 
			
		||||
            .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
 | 
			
		||||
        verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
        horizontalArrangement = Arrangement.spacedBy(24.dp),
 | 
			
		||||
    ) {
 | 
			
		||||
        Icon(
 | 
			
		||||
            imageVector = Icons.Outlined.PeopleAlt,
 | 
			
		||||
            contentDescription = null,
 | 
			
		||||
            tint = if (active) {
 | 
			
		||||
                MaterialTheme.colorScheme.active
 | 
			
		||||
            } else {
 | 
			
		||||
                LocalContentColor.current
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        Text(
 | 
			
		||||
            text = stringResource(R.string.scanlator),
 | 
			
		||||
            style = MaterialTheme.typography.bodyMedium,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,6 @@ import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.util.fastAll
 | 
			
		||||
import androidx.compose.ui.util.fastAny
 | 
			
		||||
import androidx.compose.ui.util.fastMap
 | 
			
		||||
import eu.kanade.domain.manga.model.chaptersFiltered
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.manga.components.ChapterHeader
 | 
			
		||||
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
 | 
			
		||||
@@ -308,7 +307,7 @@ private fun MangaScreenSmallImpl(
 | 
			
		||||
                title = state.manga.title,
 | 
			
		||||
                titleAlphaProvider = { animatedTitleAlpha },
 | 
			
		||||
                backgroundAlphaProvider = { animatedBgAlpha },
 | 
			
		||||
                hasFilters = state.manga.chaptersFiltered(),
 | 
			
		||||
                hasFilters = state.filterActive,
 | 
			
		||||
                onBackClicked = internalOnBackPressed,
 | 
			
		||||
                onClickFilter = onFilterClicked,
 | 
			
		||||
                onClickShare = onShareClicked,
 | 
			
		||||
@@ -561,7 +560,7 @@ fun MangaScreenLargeImpl(
 | 
			
		||||
                    title = state.manga.title,
 | 
			
		||||
                    titleAlphaProvider = { if (isAnySelected) 1f else 0f },
 | 
			
		||||
                    backgroundAlphaProvider = { 1f },
 | 
			
		||||
                    hasFilters = state.manga.chaptersFiltered(),
 | 
			
		||||
                    hasFilters = state.filterActive,
 | 
			
		||||
                    onBackClicked = internalOnBackPressed,
 | 
			
		||||
                    onClickFilter = onFilterButtonClicked,
 | 
			
		||||
                    onClickShare = onShareClicked,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,134 @@
 | 
			
		||||
package eu.kanade.presentation.manga.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.clickable
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.FlowRow
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.Spacer
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyColumn
 | 
			
		||||
import androidx.compose.foundation.lazy.rememberLazyListState
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
 | 
			
		||||
import androidx.compose.material.icons.rounded.DisabledByDefault
 | 
			
		||||
import androidx.compose.material3.AlertDialog
 | 
			
		||||
import androidx.compose.material3.HorizontalDivider
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.LocalContentColor
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.material3.minimumInteractiveComponentSize
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.runtime.toMutableStateList
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.window.DialogProperties
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import tachiyomi.presentation.core.components.material.TextButton
 | 
			
		||||
import tachiyomi.presentation.core.components.material.padding
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToEnd
 | 
			
		||||
import tachiyomi.presentation.core.util.isScrolledToStart
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun ScanlatorFilterDialog(
 | 
			
		||||
    availableScanlators: Set<String>,
 | 
			
		||||
    excludedScanlators: Set<String>,
 | 
			
		||||
    onDismissRequest: () -> Unit,
 | 
			
		||||
    onConfirm: (Set<String>) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val sortedAvailableScanlators = remember(availableScanlators) {
 | 
			
		||||
        availableScanlators.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it })
 | 
			
		||||
    }
 | 
			
		||||
    val mutableExcludedScanlators = remember(excludedScanlators) { excludedScanlators.toMutableStateList() }
 | 
			
		||||
    AlertDialog(
 | 
			
		||||
        onDismissRequest = onDismissRequest,
 | 
			
		||||
        title = { Text(text = stringResource(R.string.exclude_scanlators)) },
 | 
			
		||||
        text = textFunc@{
 | 
			
		||||
            if (sortedAvailableScanlators.isEmpty()) {
 | 
			
		||||
                Text(text = stringResource(R.string.no_scanlators_found))
 | 
			
		||||
                return@textFunc
 | 
			
		||||
            }
 | 
			
		||||
            Box {
 | 
			
		||||
                val state = rememberLazyListState()
 | 
			
		||||
                LazyColumn(state = state) {
 | 
			
		||||
                    sortedAvailableScanlators.forEach { scanlator ->
 | 
			
		||||
                        item {
 | 
			
		||||
                            val isExcluded = mutableExcludedScanlators.contains(scanlator)
 | 
			
		||||
                            Row(
 | 
			
		||||
                                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                                modifier = Modifier
 | 
			
		||||
                                    .clickable {
 | 
			
		||||
                                        if (isExcluded) {
 | 
			
		||||
                                            mutableExcludedScanlators.remove(scanlator)
 | 
			
		||||
                                        } else {
 | 
			
		||||
                                            mutableExcludedScanlators.add(scanlator)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }
 | 
			
		||||
                                    .minimumInteractiveComponentSize()
 | 
			
		||||
                                    .clip(MaterialTheme.shapes.small)
 | 
			
		||||
                                    .fillMaxWidth()
 | 
			
		||||
                                    .padding(horizontal = MaterialTheme.padding.small),
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    imageVector = if (isExcluded) {
 | 
			
		||||
                                        Icons.Rounded.DisabledByDefault
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        Icons.Rounded.CheckBoxOutlineBlank
 | 
			
		||||
                                    },
 | 
			
		||||
                                    tint = if (isExcluded) {
 | 
			
		||||
                                        MaterialTheme.colorScheme.primary
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        LocalContentColor.current
 | 
			
		||||
                                    },
 | 
			
		||||
                                    contentDescription = null,
 | 
			
		||||
                                )
 | 
			
		||||
                                Text(
 | 
			
		||||
                                    text = scanlator,
 | 
			
		||||
                                    style = MaterialTheme.typography.bodyMedium,
 | 
			
		||||
                                    modifier = Modifier.padding(start = 24.dp),
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
 | 
			
		||||
                if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        properties = DialogProperties(
 | 
			
		||||
            usePlatformDefaultWidth = true,
 | 
			
		||||
        ),
 | 
			
		||||
        confirmButton = {
 | 
			
		||||
            FlowRow {
 | 
			
		||||
                if (sortedAvailableScanlators.isEmpty()) {
 | 
			
		||||
                    TextButton(onClick = onDismissRequest) {
 | 
			
		||||
                        Text(text = stringResource(R.string.action_cancel))
 | 
			
		||||
                    }
 | 
			
		||||
                    return@FlowRow
 | 
			
		||||
                }
 | 
			
		||||
                TextButton(onClick = mutableExcludedScanlators::clear) {
 | 
			
		||||
                    Text(text = stringResource(R.string.action_reset))
 | 
			
		||||
                }
 | 
			
		||||
                Spacer(modifier = Modifier.weight(1f))
 | 
			
		||||
                TextButton(onClick = onDismissRequest) {
 | 
			
		||||
                    Text(text = stringResource(R.string.action_cancel))
 | 
			
		||||
                }
 | 
			
		||||
                TextButton(
 | 
			
		||||
                    onClick = {
 | 
			
		||||
                        onConfirm(mutableExcludedScanlators.toSet())
 | 
			
		||||
                        onDismissRequest()
 | 
			
		||||
                    },
 | 
			
		||||
                ) {
 | 
			
		||||
                    Text(text = stringResource(R.string.action_ok))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.models.Backup
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
 | 
			
		||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
 | 
			
		||||
@@ -189,10 +190,15 @@ class BackupCreator(
 | 
			
		||||
        // Check if user wants chapter information in backup
 | 
			
		||||
        if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
 | 
			
		||||
            // Backup all the chapters
 | 
			
		||||
            val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
 | 
			
		||||
            if (chapters.isNotEmpty()) {
 | 
			
		||||
                mangaObject.chapters = chapters
 | 
			
		||||
            handler.awaitList {
 | 
			
		||||
                chaptersQueries.getChaptersByMangaId(
 | 
			
		||||
                    mangaId = manga.id,
 | 
			
		||||
                    applyScanlatorFilter = 0, // false
 | 
			
		||||
                    mapper = backupChapterMapper,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
                .takeUnless(List<BackupChapter>::isEmpty)
 | 
			
		||||
                ?.let { mangaObject.chapters = it }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if user wants category information in backup
 | 
			
		||||
 
 | 
			
		||||
@@ -414,7 +414,7 @@ class LibraryScreenModel(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getNextUnreadChapter(manga: Manga): Chapter? {
 | 
			
		||||
        return getChaptersByMangaId.await(manga.id).getNextUnread(manga, downloadManager)
 | 
			
		||||
        return getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true).getNextUnread(manga, downloadManager)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,10 @@ import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
@@ -30,6 +32,7 @@ import eu.kanade.presentation.manga.EditCoverAction
 | 
			
		||||
import eu.kanade.presentation.manga.MangaScreen
 | 
			
		||||
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
 | 
			
		||||
import eu.kanade.presentation.manga.components.MangaCoverDialog
 | 
			
		||||
import eu.kanade.presentation.manga.components.ScanlatorFilterDialog
 | 
			
		||||
import eu.kanade.presentation.manga.components.SetIntervalDialog
 | 
			
		||||
import eu.kanade.presentation.util.AssistContentScreen
 | 
			
		||||
import eu.kanade.presentation.util.Screen
 | 
			
		||||
@@ -152,6 +155,8 @@ class MangaScreen(
 | 
			
		||||
            onInvertSelection = screenModel::invertSelection,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        var showScanlatorsDialog by remember { mutableStateOf(false) }
 | 
			
		||||
 | 
			
		||||
        val onDismissRequest = { screenModel.dismissDialog() }
 | 
			
		||||
        when (val dialog = successState.dialog) {
 | 
			
		||||
            null -> {}
 | 
			
		||||
@@ -189,6 +194,8 @@ class MangaScreen(
 | 
			
		||||
                onDisplayModeChanged = screenModel::setDisplayMode,
 | 
			
		||||
                onSetAsDefault = screenModel::setCurrentSettingsAsDefault,
 | 
			
		||||
                onResetToDefault = screenModel::resetToDefaultSettings,
 | 
			
		||||
                scanlatorFilterActive = successState.scanlatorFilterActive,
 | 
			
		||||
                onScanlatorFilterClicked = { showScanlatorsDialog = true },
 | 
			
		||||
            )
 | 
			
		||||
            MangaScreenModel.Dialog.TrackSheet -> {
 | 
			
		||||
                NavigatorAdaptiveSheet(
 | 
			
		||||
@@ -235,6 +242,15 @@ class MangaScreen(
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (showScanlatorsDialog) {
 | 
			
		||||
            ScanlatorFilterDialog(
 | 
			
		||||
                availableScanlators = successState.availableScanlators,
 | 
			
		||||
                excludedScanlators = successState.excludedScanlators,
 | 
			
		||||
                onDismissRequest = { showScanlatorsDialog = false },
 | 
			
		||||
                onConfirm = screenModel::setExcludedScanlators,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun continueReading(context: Context, unreadChapter: Chapter?) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,13 @@ import cafe.adriel.voyager.core.model.screenModelScope
 | 
			
		||||
import eu.kanade.core.preference.asState
 | 
			
		||||
import eu.kanade.core.util.addOrRemove
 | 
			
		||||
import eu.kanade.core.util.insertSeparators
 | 
			
		||||
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
 | 
			
		||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
 | 
			
		||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
 | 
			
		||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
 | 
			
		||||
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
 | 
			
		||||
import eu.kanade.domain.manga.interactor.UpdateManga
 | 
			
		||||
import eu.kanade.domain.manga.model.chaptersFiltered
 | 
			
		||||
import eu.kanade.domain.manga.model.downloadedFilter
 | 
			
		||||
import eu.kanade.domain.manga.model.toSManga
 | 
			
		||||
import eu.kanade.domain.track.interactor.AddTracks
 | 
			
		||||
@@ -92,6 +96,9 @@ class MangaScreenModel(
 | 
			
		||||
    private val downloadCache: DownloadCache = Injekt.get(),
 | 
			
		||||
    private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(),
 | 
			
		||||
    private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
 | 
			
		||||
    private val getAvailableScanlators: GetAvailableScanlators = Injekt.get(),
 | 
			
		||||
    private val getExcludedScanlators: GetExcludedScanlators = Injekt.get(),
 | 
			
		||||
    private val setExcludedScanlators: SetExcludedScanlators = Injekt.get(),
 | 
			
		||||
    private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(),
 | 
			
		||||
    private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
 | 
			
		||||
    private val setReadStatus: SetReadStatus = Injekt.get(),
 | 
			
		||||
@@ -154,7 +161,7 @@ class MangaScreenModel(
 | 
			
		||||
    init {
 | 
			
		||||
        screenModelScope.launchIO {
 | 
			
		||||
            combine(
 | 
			
		||||
                getMangaAndChapters.subscribe(mangaId).distinctUntilChanged(),
 | 
			
		||||
                getMangaAndChapters.subscribe(mangaId, applyScanlatorFilter = true).distinctUntilChanged(),
 | 
			
		||||
                downloadCache.changes,
 | 
			
		||||
                downloadManager.queueState,
 | 
			
		||||
            ) { mangaAndChapters, _, _ -> mangaAndChapters }
 | 
			
		||||
@@ -168,11 +175,31 @@ class MangaScreenModel(
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        screenModelScope.launchIO {
 | 
			
		||||
            getExcludedScanlators.subscribe(mangaId)
 | 
			
		||||
                .distinctUntilChanged()
 | 
			
		||||
                .collectLatest { excludedScanlators ->
 | 
			
		||||
                    updateSuccessState {
 | 
			
		||||
                        it.copy(excludedScanlators = excludedScanlators)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        screenModelScope.launchIO {
 | 
			
		||||
            getAvailableScanlators.subscribe(mangaId)
 | 
			
		||||
                .distinctUntilChanged()
 | 
			
		||||
                .collectLatest { availableScanlators ->
 | 
			
		||||
                    updateSuccessState {
 | 
			
		||||
                        it.copy(availableScanlators = availableScanlators)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        observeDownloads()
 | 
			
		||||
 | 
			
		||||
        screenModelScope.launchIO {
 | 
			
		||||
            val manga = getMangaAndChapters.awaitManga(mangaId)
 | 
			
		||||
            val chapters = getMangaAndChapters.awaitChapters(mangaId)
 | 
			
		||||
            val chapters = getMangaAndChapters.awaitChapters(mangaId, applyScanlatorFilter = true)
 | 
			
		||||
                .toChapterListItems(manga)
 | 
			
		||||
 | 
			
		||||
            if (!manga.favorite) {
 | 
			
		||||
@@ -189,6 +216,8 @@ class MangaScreenModel(
 | 
			
		||||
                    source = Injekt.get<SourceManager>().getOrStub(manga.source),
 | 
			
		||||
                    isFromSource = isFromSource,
 | 
			
		||||
                    chapters = chapters,
 | 
			
		||||
                    availableScanlators = getAvailableScanlators.await(mangaId),
 | 
			
		||||
                    excludedScanlators = getExcludedScanlators.await(mangaId),
 | 
			
		||||
                    isRefreshingData = needRefreshInfo || needRefreshChapter,
 | 
			
		||||
                    dialog = null,
 | 
			
		||||
                )
 | 
			
		||||
@@ -995,6 +1024,12 @@ class MangaScreenModel(
 | 
			
		||||
        updateSuccessState { it.copy(dialog = Dialog.FullCover) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setExcludedScanlators(excludedScanlators: Set<String>) {
 | 
			
		||||
        screenModelScope.launchIO {
 | 
			
		||||
            setExcludedScanlators.await(mangaId, excludedScanlators)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sealed interface State {
 | 
			
		||||
        @Immutable
 | 
			
		||||
        data object Loading : State
 | 
			
		||||
@@ -1005,12 +1040,13 @@ class MangaScreenModel(
 | 
			
		||||
            val source: Source,
 | 
			
		||||
            val isFromSource: Boolean,
 | 
			
		||||
            val chapters: List<ChapterList.Item>,
 | 
			
		||||
            val availableScanlators: Set<String>,
 | 
			
		||||
            val excludedScanlators: Set<String>,
 | 
			
		||||
            val trackItems: List<TrackItem> = emptyList(),
 | 
			
		||||
            val isRefreshingData: Boolean = false,
 | 
			
		||||
            val dialog: Dialog? = null,
 | 
			
		||||
            val hasPromptedToAddBefore: Boolean = false,
 | 
			
		||||
        ) : State {
 | 
			
		||||
 | 
			
		||||
            val processedChapters by lazy {
 | 
			
		||||
                chapters.applyFilters(manga).toList()
 | 
			
		||||
            }
 | 
			
		||||
@@ -1042,6 +1078,12 @@ class MangaScreenModel(
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val scanlatorFilterActive: Boolean
 | 
			
		||||
                get() = excludedScanlators.intersect(availableScanlators).isNotEmpty()
 | 
			
		||||
 | 
			
		||||
            val filterActive: Boolean
 | 
			
		||||
                get() = scanlatorFilterActive || manga.chaptersFiltered()
 | 
			
		||||
 | 
			
		||||
            val trackingAvailable: Boolean
 | 
			
		||||
                get() = trackItems.isNotEmpty()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -147,7 +147,7 @@ class ReaderViewModel @JvmOverloads constructor(
 | 
			
		||||
     */
 | 
			
		||||
    private val chapterList by lazy {
 | 
			
		||||
        val manga = manga!!
 | 
			
		||||
        val chapters = runBlocking { getChaptersByMangaId.await(manga.id) }
 | 
			
		||||
        val chapters = runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) }
 | 
			
		||||
 | 
			
		||||
        val selectedChapter = chapters.find { it.id == chapterId }
 | 
			
		||||
            ?: error("Requested chapter of id $chapterId not found in chapter list")
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package tachiyomi.data.chapter
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import logcat.LogPriority
 | 
			
		||||
import tachiyomi.core.util.lang.toLong
 | 
			
		||||
import tachiyomi.core.util.system.logcat
 | 
			
		||||
import tachiyomi.data.DatabaseHandler
 | 
			
		||||
import tachiyomi.domain.chapter.model.Chapter
 | 
			
		||||
@@ -76,8 +77,22 @@ class ChapterRepositoryImpl(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getChapterByMangaId(mangaId: Long): List<Chapter> {
 | 
			
		||||
        return handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, ::mapChapter) }
 | 
			
		||||
    override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List<Chapter> {
 | 
			
		||||
        return handler.awaitList {
 | 
			
		||||
            chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getScanlatorsByMangaId(mangaId: Long): List<String> {
 | 
			
		||||
        return handler.awaitList {
 | 
			
		||||
            chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow<List<String>> {
 | 
			
		||||
        return handler.subscribeToList {
 | 
			
		||||
            chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
 | 
			
		||||
@@ -93,12 +108,9 @@ class ChapterRepositoryImpl(
 | 
			
		||||
        return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, ::mapChapter) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
 | 
			
		||||
    override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow<List<Chapter>> {
 | 
			
		||||
        return handler.subscribeToList {
 | 
			
		||||
            chaptersQueries.getChaptersByMangaId(
 | 
			
		||||
                mangaId,
 | 
			
		||||
                ::mapChapter,
 | 
			
		||||
            )
 | 
			
		||||
            chaptersQueries.getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,19 @@ FROM chapters
 | 
			
		||||
WHERE _id = :id;
 | 
			
		||||
 | 
			
		||||
getChaptersByMangaId:
 | 
			
		||||
SELECT *
 | 
			
		||||
SELECT C.*
 | 
			
		||||
FROM chapters C
 | 
			
		||||
LEFT JOIN excluded_scanlators ES
 | 
			
		||||
ON C.manga_id = ES.manga_id
 | 
			
		||||
AND C.scanlator = ES.scanlator
 | 
			
		||||
WHERE C.manga_id = :mangaId
 | 
			
		||||
AND (
 | 
			
		||||
    :applyScanlatorFilter = 0
 | 
			
		||||
    OR ES.scanlator IS NULL
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
getScanlatorsByMangaId:
 | 
			
		||||
SELECT scanlator
 | 
			
		||||
FROM chapters
 | 
			
		||||
WHERE manga_id = :mangaId;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
CREATE TABLE excluded_scanlators(
 | 
			
		||||
    manga_id INTEGER NOT NULL,
 | 
			
		||||
    scanlator TEXT NOT NULL,
 | 
			
		||||
    FOREIGN KEY(manga_id) REFERENCES mangas (_id)
 | 
			
		||||
    ON DELETE CASCADE
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id);
 | 
			
		||||
 | 
			
		||||
insert:
 | 
			
		||||
INSERT INTO excluded_scanlators(manga_id, scanlator)
 | 
			
		||||
VALUES (:mangaId, :scanlator);
 | 
			
		||||
 | 
			
		||||
remove:
 | 
			
		||||
DELETE FROM excluded_scanlators
 | 
			
		||||
WHERE manga_id = :mangaId
 | 
			
		||||
AND scanlator IN :scanlators;
 | 
			
		||||
 | 
			
		||||
getExcludedScanlatorsByMangaId:
 | 
			
		||||
SELECT scanlator
 | 
			
		||||
FROM excluded_scanlators
 | 
			
		||||
WHERE manga_id = :mangaId;
 | 
			
		||||
@@ -20,4 +20,4 @@ FROM mangas JOIN chapters
 | 
			
		||||
ON mangas._id = chapters.manga_id
 | 
			
		||||
WHERE favorite = 1
 | 
			
		||||
AND date_fetch > date_added
 | 
			
		||||
ORDER BY date_fetch DESC;
 | 
			
		||||
ORDER BY date_fetch DESC;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								data/src/main/sqldelight/tachiyomi/migrations/26.sqm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								data/src/main/sqldelight/tachiyomi/migrations/26.sqm
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
CREATE TABLE excluded_scanlators(
 | 
			
		||||
    manga_id INTEGER NOT NULL,
 | 
			
		||||
    scanlator TEXT NOT NULL,
 | 
			
		||||
    FOREIGN KEY(manga_id) REFERENCES mangas (_id)
 | 
			
		||||
    ON DELETE CASCADE
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id);
 | 
			
		||||
 | 
			
		||||
DROP VIEW IF EXISTS libraryView;
 | 
			
		||||
 | 
			
		||||
CREATE VIEW libraryView AS
 | 
			
		||||
SELECT
 | 
			
		||||
    M.*,
 | 
			
		||||
    coalesce(C.total, 0) AS totalCount,
 | 
			
		||||
    coalesce(C.readCount, 0) AS readCount,
 | 
			
		||||
    coalesce(C.latestUpload, 0) AS latestUpload,
 | 
			
		||||
    coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
 | 
			
		||||
    coalesce(C.lastRead, 0) AS lastRead,
 | 
			
		||||
    coalesce(C.bookmarkCount, 0) AS bookmarkCount,
 | 
			
		||||
    coalesce(MC.category_id, 0) AS category
 | 
			
		||||
FROM mangas M
 | 
			
		||||
LEFT JOIN(
 | 
			
		||||
    SELECT
 | 
			
		||||
        chapters.manga_id,
 | 
			
		||||
        count(*) AS total,
 | 
			
		||||
        sum(read) AS readCount,
 | 
			
		||||
        coalesce(max(chapters.date_upload), 0) AS latestUpload,
 | 
			
		||||
        coalesce(max(history.last_read), 0) AS lastRead,
 | 
			
		||||
        coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
 | 
			
		||||
        sum(chapters.bookmark) AS bookmarkCount
 | 
			
		||||
    FROM chapters
 | 
			
		||||
    LEFT JOIN excluded_scanlators
 | 
			
		||||
    ON chapters.manga_id = excluded_scanlators.manga_id
 | 
			
		||||
    AND chapters.scanlator = excluded_scanlators.scanlator
 | 
			
		||||
    LEFT JOIN history
 | 
			
		||||
    ON chapters._id = history.chapter_id
 | 
			
		||||
    WHERE excluded_scanlators.scanlator IS NULL
 | 
			
		||||
    GROUP BY chapters.manga_id
 | 
			
		||||
) AS C
 | 
			
		||||
ON M._id = C.manga_id
 | 
			
		||||
LEFT JOIN mangas_categories AS MC
 | 
			
		||||
ON MC.manga_id = M._id
 | 
			
		||||
WHERE M.favorite = 1;
 | 
			
		||||
@@ -19,8 +19,12 @@ LEFT JOIN(
 | 
			
		||||
        coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
 | 
			
		||||
        sum(chapters.bookmark) AS bookmarkCount
 | 
			
		||||
    FROM chapters
 | 
			
		||||
    LEFT JOIN excluded_scanlators
 | 
			
		||||
    ON chapters.manga_id = excluded_scanlators.manga_id
 | 
			
		||||
    AND chapters.scanlator = excluded_scanlators.scanlator
 | 
			
		||||
    LEFT JOIN history
 | 
			
		||||
    ON chapters._id = history.chapter_id
 | 
			
		||||
    WHERE excluded_scanlators.scanlator IS NULL
 | 
			
		||||
    GROUP BY chapters.manga_id
 | 
			
		||||
) AS C
 | 
			
		||||
ON M._id = C.manga_id
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,9 @@ class GetChaptersByMangaId(
 | 
			
		||||
    private val chapterRepository: ChapterRepository,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    suspend fun await(mangaId: Long): List<Chapter> {
 | 
			
		||||
    suspend fun await(mangaId: Long, applyScanlatorFilter: Boolean = false): List<Chapter> {
 | 
			
		||||
        return try {
 | 
			
		||||
            chapterRepository.getChapterByMangaId(mangaId)
 | 
			
		||||
            chapterRepository.getChapterByMangaId(mangaId, applyScanlatorFilter)
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            logcat(LogPriority.ERROR, e)
 | 
			
		||||
            emptyList()
 | 
			
		||||
 
 | 
			
		||||
@@ -14,13 +14,17 @@ interface ChapterRepository {
 | 
			
		||||
 | 
			
		||||
    suspend fun removeChaptersWithIds(chapterIds: List<Long>)
 | 
			
		||||
 | 
			
		||||
    suspend fun getChapterByMangaId(mangaId: Long): List<Chapter>
 | 
			
		||||
    suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean = false): List<Chapter>
 | 
			
		||||
 | 
			
		||||
    suspend fun getScanlatorsByMangaId(mangaId: Long): List<String>
 | 
			
		||||
 | 
			
		||||
    fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow<List<String>>
 | 
			
		||||
 | 
			
		||||
    suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter>
 | 
			
		||||
 | 
			
		||||
    suspend fun getChapterById(id: Long): Chapter?
 | 
			
		||||
 | 
			
		||||
    suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>>
 | 
			
		||||
    suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean = false): Flow<List<Chapter>>
 | 
			
		||||
 | 
			
		||||
    suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter?
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ class GetNextChapters(
 | 
			
		||||
 | 
			
		||||
    suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List<Chapter> {
 | 
			
		||||
        val manga = getManga.await(mangaId) ?: return emptyList()
 | 
			
		||||
        val chapters = getChaptersByMangaId.await(mangaId)
 | 
			
		||||
        val chapters = getChaptersByMangaId.await(mangaId, applyScanlatorFilter = true)
 | 
			
		||||
            .sortedWith(getChapterSort(manga, sortDescending = false))
 | 
			
		||||
 | 
			
		||||
        return if (onlyUnread) {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class FetchInterval(
 | 
			
		||||
        } else {
 | 
			
		||||
            window
 | 
			
		||||
        }
 | 
			
		||||
        val chapters = getChaptersByMangaId.await(manga.id)
 | 
			
		||||
        val chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true)
 | 
			
		||||
        val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
 | 
			
		||||
            chapters,
 | 
			
		||||
            dateTime.zone,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,10 @@ class GetMangaWithChapters(
 | 
			
		||||
    private val chapterRepository: ChapterRepository,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    suspend fun subscribe(id: Long): Flow<Pair<Manga, List<Chapter>>> {
 | 
			
		||||
    suspend fun subscribe(id: Long, applyScanlatorFilter: Boolean = false): Flow<Pair<Manga, List<Chapter>>> {
 | 
			
		||||
        return combine(
 | 
			
		||||
            mangaRepository.getMangaByIdAsFlow(id),
 | 
			
		||||
            chapterRepository.getChapterByMangaIdAsFlow(id),
 | 
			
		||||
            chapterRepository.getChapterByMangaIdAsFlow(id, applyScanlatorFilter),
 | 
			
		||||
        ) { manga, chapters ->
 | 
			
		||||
            Pair(manga, chapters)
 | 
			
		||||
        }
 | 
			
		||||
@@ -25,7 +25,7 @@ class GetMangaWithChapters(
 | 
			
		||||
        return mangaRepository.getMangaById(id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun awaitChapters(id: Long): List<Chapter> {
 | 
			
		||||
        return chapterRepository.getChapterByMangaId(id)
 | 
			
		||||
    suspend fun awaitChapters(id: Long, applyScanlatorFilter: Boolean = false): List<Chapter> {
 | 
			
		||||
        return chapterRepository.getChapterByMangaId(id, applyScanlatorFilter)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@
 | 
			
		||||
    <string name="track">Tracking</string>
 | 
			
		||||
    <string name="delete_downloaded">Delete downloaded</string>
 | 
			
		||||
    <string name="history">History</string>
 | 
			
		||||
    <string name="scanlator">Scanlator</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Screen titles -->
 | 
			
		||||
    <string name="label_more">More</string>
 | 
			
		||||
@@ -702,6 +703,8 @@
 | 
			
		||||
    <string name="set_chapter_settings_as_default">Set as default</string>
 | 
			
		||||
    <string name="no_chapters_error">No chapters found</string>
 | 
			
		||||
    <string name="are_you_sure">Are you sure?</string>
 | 
			
		||||
    <string name="exclude_scanlators">Exclude scanlators</string>
 | 
			
		||||
    <string name="no_scanlators_found">No scanlators found</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Tracking Screen -->
 | 
			
		||||
    <string name="manga_tracking_tab">Tracking</string>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user