From c1659ad9083903cb9e292c73c24ec105e94c619d Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 24 Apr 2022 20:35:59 +0200 Subject: [PATCH] Convert Source tab to use Compose (#6987) * Use Compose in Source tab * Replace hashCode with key function * Add ability to turn off pins moving on top of source list * Changes from review comments (cherry picked from commit 29a0989f2889d3361f583285091878c9b4570a52) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt --- .../eu/kanade/data/source/SourceMapper.kt | 13 + .../data/source/SourceRepositoryImpl.kt | 18 + .../java/eu/kanade/domain/DomainModule.kt | 20 + .../domain/source/interactor/DisableSource.kt | 14 + .../source/interactor/GetEnabledSources.kt | 94 ++++ .../domain/source/interactor/GetShowLatest.kt | 18 + .../source/interactor/GetSourceCategories.kt | 13 + .../source/interactor/SetSourceCategories.kt | 22 + .../interactor/ToggleExcludeFromDataSaver.kt | 20 + .../source/interactor/ToggleSourcePin.kt | 20 + .../eu/kanade/domain/source/model/Source.kt | 84 ++++ .../source/repository/SourceRepository.kt | 9 + .../presentation/source/SourceScreen.kt | 402 ++++++++++++++++++ .../presentation/theme/TachiyomiTheme.kt | 3 +- .../kanade/presentation/theme/Typography.kt | 16 + .../data/preference/PreferencesHelper.kt | 2 + .../tachiyomi/extension/ExtensionManager.kt | 8 +- .../kanade/tachiyomi/source/SourceManager.kt | 14 + .../ui/base/controller/ComposeController.kt | 23 +- .../tachiyomi/ui/browse/source/LangHolder.kt | 17 - .../tachiyomi/ui/browse/source/LangItem.kt | 42 -- .../ui/browse/source/SourceAdapter.kt | 32 -- .../ui/browse/source/SourceController.kt | 340 ++------------- .../ui/browse/source/SourceHolder.kt | 58 --- .../tachiyomi/ui/browse/source/SourceItem.kt | 60 --- .../ui/browse/source/SourcePresenter.kt | 260 +++++------ .../source/browse/BrowseSourceController.kt | 44 +- .../source/latest/LatestUpdatesController.kt | 7 + .../ui/setting/SettingsBrowseController.kt | 11 + .../main/res/drawable/ic_push_pin_24dp.xml | 9 - .../res/drawable/ic_push_pin_outline_24dp.xml | 9 - .../res/layout/source_main_controller.xml | 25 -- .../layout/source_main_controller_item.xml | 18 +- app/src/main/res/values/strings.xml | 2 + 34 files changed, 1054 insertions(+), 693 deletions(-) create mode 100644 app/src/main/java/eu/kanade/data/source/SourceMapper.kt create mode 100644 app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/GetShowLatest.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/GetSourceCategories.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/SetSourceCategories.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/ToggleExcludeFromDataSaver.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/ToggleSourcePin.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/model/Source.kt create mode 100644 app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt create mode 100644 app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/theme/Typography.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/LangHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/LangItem.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceAdapter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt delete mode 100644 app/src/main/res/drawable/ic_push_pin_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_push_pin_outline_24dp.xml delete mode 100644 app/src/main/res/layout/source_main_controller.xml diff --git a/app/src/main/java/eu/kanade/data/source/SourceMapper.kt b/app/src/main/java/eu/kanade/data/source/SourceMapper.kt new file mode 100644 index 000000000..a54022cd6 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/source/SourceMapper.kt @@ -0,0 +1,13 @@ +package eu.kanade.data.source + +import eu.kanade.domain.source.model.Source +import eu.kanade.tachiyomi.source.CatalogueSource + +val sourceMapper: (CatalogueSource) -> Source = { source -> + Source( + source.id, + source.lang, + source.name, + source.supportsLatest + ) +} diff --git a/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt new file mode 100644 index 000000000..b31f24f88 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt @@ -0,0 +1,18 @@ +package eu.kanade.data.source + +import eu.kanade.domain.source.model.Source +import eu.kanade.domain.source.repository.SourceRepository +import eu.kanade.tachiyomi.source.SourceManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class SourceRepositoryImpl( + private val sourceManager: SourceManager +) : SourceRepository { + + override fun getSources(): Flow> { + return sourceManager.catalogueSources.map { sources -> + sources.map(sourceMapper) + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 9462ae7f9..1ac0af401 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -1,12 +1,21 @@ package eu.kanade.domain import eu.kanade.data.history.HistoryRepositoryImpl +import eu.kanade.data.source.SourceRepositoryImpl import eu.kanade.domain.history.interactor.DeleteHistoryTable import eu.kanade.domain.history.interactor.GetHistory import eu.kanade.domain.history.interactor.GetNextChapterForManga import eu.kanade.domain.history.interactor.RemoveHistoryById import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId import eu.kanade.domain.history.repository.HistoryRepository +import eu.kanade.domain.source.interactor.DisableSource +import eu.kanade.domain.source.interactor.GetEnabledSources +import eu.kanade.domain.source.interactor.GetShowLatest +import eu.kanade.domain.source.interactor.GetSourceCategories +import eu.kanade.domain.source.interactor.SetSourceCategories +import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver +import eu.kanade.domain.source.interactor.ToggleSourcePin +import eu.kanade.domain.source.repository.SourceRepository import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addFactory @@ -22,5 +31,16 @@ class DomainModule : InjektModule { addFactory { GetNextChapterForManga(get()) } addFactory { RemoveHistoryById(get()) } addFactory { RemoveHistoryByMangaId(get()) } + addSingletonFactory { SourceRepositoryImpl(get()) } + addFactory { GetEnabledSources(get(), get()) } + addFactory { DisableSource(get()) } + addFactory { ToggleSourcePin(get()) } + + // SY --> + addFactory { GetSourceCategories(get()) } + addFactory { GetShowLatest(get()) } + addFactory { ToggleExcludeFromDataSaver(get()) } + addFactory { SetSourceCategories(get()) } + // SY <-- } } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt b/app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt new file mode 100644 index 000000000..336be8b99 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/DisableSource.kt @@ -0,0 +1,14 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.domain.source.model.Source +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.plusAssign + +class DisableSource( + private val preferences: PreferencesHelper +) { + + fun await(source: Source) { + preferences.disabledSources() += source.id.toString() + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt new file mode 100644 index 000000000..75f8f2c1a --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt @@ -0,0 +1,94 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.domain.source.model.Pin +import eu.kanade.domain.source.model.Pins +import eu.kanade.domain.source.model.Source +import eu.kanade.domain.source.repository.SourceRepository +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.LocalSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged + +class GetEnabledSources( + private val repository: SourceRepository, + private val preferences: PreferencesHelper +) { + + fun subscribe(): Flow> { + return preferences.pinnedSources().asFlow() + .combine(preferences.enabledLanguages().asFlow()) { pinList, enabledLanguages -> + Config(pinSet = pinList, enabledSources = enabledLanguages) + } + .combine(preferences.disabledSources().asFlow()) { config, disabledSources -> + config.copy(disabledSources = disabledSources) + } + .combine(preferences.lastUsedSource().asFlow()) { config, lastUsedSource -> + config.copy(lastUsedSource = lastUsedSource) + } + // SY --> + .combine(preferences.dataSaverExcludedSources().asFlow()) { config, excludedFromDataSaver -> + config.copy(excludedFromDataSaver = excludedFromDataSaver) + } + .combine(preferences.sourcesTabSourcesInCategories().asFlow()) { config, sourcesInCategories -> + config.copy(sourcesInCategories = sourcesInCategories) + } + .combine(preferences.sourcesTabCategoriesFilter().asFlow()) { config, sourceCategoriesFilter -> + config.copy(sourceCategoriesFilter = sourceCategoriesFilter) + } + // SY <-- + .combine(repository.getSources()) { (pinList, enabledLanguages, disabledSources, lastUsedSource, excludedFromDataSaver, sourcesInCategories, sourceCategoriesFilter), sources -> + val pinsOnTop = preferences.pinsOnTop().get() + val sourcesAndCategories = sourcesInCategories.map { + it.split('|').let { (source, test) -> source.toLong() to test } + } + val sourcesInSourceCategories = sourcesAndCategories.map { it.first } + sources + .filter { it.lang in enabledLanguages || it.id == LocalSource.ID } + .filterNot { it.id.toString() in disabledSources } + .flatMap { + val flag = if ("${it.id}" in pinList) Pins.pinned else Pins.unpinned + // SY --> + val categories = sourcesAndCategories.filter { (id) -> id == it.id } + .map(Pair<*, String>::second) + .toSet() + // SY <-- + val source = it.copy( + pin = flag, + isExcludedFromDataSaver = it.id.toString() in excludedFromDataSaver, + categories = categories + ) + val toFlatten = mutableListOf(source) + if (source.id == lastUsedSource) { + toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual)) + } + if (pinsOnTop.not() && Pin.Pinned in source.pin) { + toFlatten[0] = toFlatten[0].copy(pin = source.pin + Pin.Forced) + toFlatten.add(source.copy(pin = source.pin - Pin.Actual)) + } + // SY --> + categories.forEach { category -> + toFlatten.add(source.copy(category = category, pin = source.pin - Pin.Actual)) + } + if (sourceCategoriesFilter && Pin.Actual !in toFlatten[0].pin && source.id in sourcesInSourceCategories) { + toFlatten.removeAt(0) + } + // SY <-- + toFlatten + } + } + .distinctUntilChanged() + } +} + +private data class Config( + val pinSet: Set = setOf(), + val enabledSources: Set = setOf(), + val disabledSources: Set = setOf(), + val lastUsedSource: Long? = null, + // SY --> + val excludedFromDataSaver: Set = setOf(), + val sourcesInCategories: Set = setOf(), + val sourceCategoriesFilter: Boolean = false, + // SY <-- +) diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetShowLatest.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetShowLatest.kt new file mode 100644 index 000000000..1d57f3214 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetShowLatest.kt @@ -0,0 +1,18 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.browse.source.SourceController +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetShowLatest( + private val preferences: PreferencesHelper +) { + + fun subscribe(mode: SourceController.Mode): Flow { + return preferences.useNewSourceNavigation().asFlow() + .map { + mode == SourceController.Mode.CATALOGUE && !it + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceCategories.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceCategories.kt new file mode 100644 index 000000000..192b0680f --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceCategories.kt @@ -0,0 +1,13 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import kotlinx.coroutines.flow.Flow + +class GetSourceCategories( + private val preferences: PreferencesHelper +) { + + fun subscribe(): Flow> { + return preferences.sourcesTabCategories().asFlow() + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/SetSourceCategories.kt b/app/src/main/java/eu/kanade/domain/source/interactor/SetSourceCategories.kt new file mode 100644 index 000000000..b70571032 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/SetSourceCategories.kt @@ -0,0 +1,22 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.domain.source.model.Source +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.minusAssign +import eu.kanade.tachiyomi.util.preference.plusAssign + +class SetSourceCategories( + private val preferences: PreferencesHelper +) { + + fun await(source: Source, sourceCategories: List) { + val sourceIdString = source.id.toString() + val currentSourceCategories = preferences.sourcesTabSourcesInCategories().get().filterNot { + it.substringBefore('|') == sourceIdString + } + val newSourceCategories = currentSourceCategories + sourceCategories.map { + "$sourceIdString|$it" + } + preferences.sourcesTabSourcesInCategories().set(newSourceCategories.toSet()) + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleExcludeFromDataSaver.kt b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleExcludeFromDataSaver.kt new file mode 100644 index 000000000..82c4a87ef --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleExcludeFromDataSaver.kt @@ -0,0 +1,20 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.domain.source.model.Source +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.minusAssign +import eu.kanade.tachiyomi.util.preference.plusAssign + +class ToggleExcludeFromDataSaver( + private val preferences: PreferencesHelper +) { + + fun await(source: Source) { + val isExcluded = source.id.toString() in preferences.dataSaverExcludedSources().get() + if (isExcluded) { + preferences.dataSaverExcludedSources() -= source.id.toString() + } else { + preferences.dataSaverExcludedSources() += source.id.toString() + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSourcePin.kt b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSourcePin.kt new file mode 100644 index 000000000..5a5e92df4 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/ToggleSourcePin.kt @@ -0,0 +1,20 @@ +package eu.kanade.domain.source.interactor + +import eu.kanade.domain.source.model.Source +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.preference.minusAssign +import eu.kanade.tachiyomi.util.preference.plusAssign + +class ToggleSourcePin( + private val preferences: PreferencesHelper +) { + + fun await(source: Source) { + val isPinned = source.id.toString() in preferences.pinnedSources().get() + if (isPinned) { + preferences.pinnedSources() -= source.id.toString() + } else { + preferences.pinnedSources() += source.id.toString() + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/model/Source.kt b/app/src/main/java/eu/kanade/domain/source/model/Source.kt new file mode 100644 index 000000000..7f036b4c7 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/model/Source.kt @@ -0,0 +1,84 @@ +package eu.kanade.domain.source.model + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.graphics.drawable.toBitmap +import eu.kanade.tachiyomi.extension.ExtensionManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +data class Source( + val id: Long, + val lang: String, + val name: String, + val supportsLatest: Boolean, + val pin: Pins = Pins.unpinned, + val isUsedLast: Boolean = false, + // SY --> + val category: String? = null, + val isExcludedFromDataSaver: Boolean = false, + val categories: Set = emptySet(), +// SY <-- +) { + + val nameWithLanguage: String + get() = "$name (${lang.uppercase()})" + + val icon: ImageBitmap? + get() { + return Injekt.get().getAppIconForSource(id) + ?.toBitmap() + ?.asImageBitmap() + } + + val key: () -> Long = { + when { + isUsedLast -> id shr 16 + Pin.Forced in pin -> id shr 32 + category != null -> id shr 48 + category.hashCode() + else -> id + } + } +} + +sealed class Pin(val code: Int) { + object Unpinned : Pin(0b00) + object Pinned : Pin(0b01) + object Actual : Pin(0b10) + object Forced : Pin(0b100) +} + +inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins { + return Pins.PinsBuilder().apply(builder).flags() +} + +fun Pins(vararg pins: Pin) = Pins { + pins.forEach { +it } +} + +data class Pins(val code: Int = Pin.Unpinned.code) { + + operator fun contains(pin: Pin): Boolean = pin.code and code == pin.code + + operator fun plus(pin: Pin): Pins = Pins(code or pin.code) + + operator fun minus(pin: Pin): Pins = Pins(code xor pin.code) + + companion object { + val unpinned = Pins(Pin.Unpinned) + + val pinned = Pins(Pin.Pinned, Pin.Actual) + } + + class PinsBuilder(var code: Int = 0) { + operator fun Pin.unaryPlus() { + this@PinsBuilder.code = code or this@PinsBuilder.code + } + + operator fun Pin.unaryMinus() { + this@PinsBuilder.code = code or this@PinsBuilder.code + } + + fun flags(): Pins = Pins(code) + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt b/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt new file mode 100644 index 000000000..dc139e93e --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt @@ -0,0 +1,9 @@ +package eu.kanade.domain.source.repository + +import eu.kanade.domain.source.model.Source +import kotlinx.coroutines.flow.Flow + +interface SourceRepository { + + fun getSources(): Flow> +} diff --git a/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt b/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt new file mode 100644 index 000000000..36d0d7187 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt @@ -0,0 +1,402 @@ +package eu.kanade.presentation.source + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PushPin +import androidx.compose.material.icons.outlined.PushPin +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import eu.kanade.domain.source.model.Pin +import eu.kanade.domain.source.model.Source +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.PreferenceRow +import eu.kanade.presentation.theme.header +import eu.kanade.presentation.util.horizontalPadding +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.ui.browse.source.SourcePresenter +import eu.kanade.tachiyomi.ui.browse.source.UiModel +import eu.kanade.tachiyomi.util.system.LocaleHelper + +@Composable +fun SourceScreen( + nestedScrollInterop: NestedScrollConnection, + presenter: SourcePresenter, + onClickItem: (Source) -> Unit, + onClickDisable: (Source) -> Unit, + onClickLatest: (Source) -> Unit, + onClickPin: (Source) -> Unit, + onClickSetCategories: (Source, List) -> Unit, + onClickToggleDataSaver: (Source) -> Unit +) { + val state by presenter.state.collectAsState() + + when { + state.isLoading -> CircularProgressIndicator() + state.hasError -> Text(text = state.error!!.message!!) + state.isEmpty -> EmptyScreen(message = "") + else -> SourceList( + nestedScrollConnection = nestedScrollInterop, + list = state.sources, + categories = state.sourceCategories, + showPin = state.showPin, + showLatest = state.showLatest, + onClickItem = onClickItem, + onClickDisable = onClickDisable, + onClickLatest = onClickLatest, + onClickPin = onClickPin, + onClickSetCategories = onClickSetCategories, + onClickToggleDataSaver = onClickToggleDataSaver + ) + } +} + +@Composable +fun SourceList( + nestedScrollConnection: NestedScrollConnection, + list: List, + categories: List, + showPin: Boolean, + showLatest: Boolean, + onClickItem: (Source) -> Unit, + onClickDisable: (Source) -> Unit, + onClickLatest: (Source) -> Unit, + onClickPin: (Source) -> Unit, + onClickSetCategories: (Source, List) -> Unit, + onClickToggleDataSaver: (Source) -> Unit +) { + val (sourceState, setSourceState) = remember { mutableStateOf(null) } + // SY --> + val (sourceCategoriesState, setSourceCategoriesState) = remember { mutableStateOf(null) } + // SY <-- + LazyColumn( + modifier = Modifier + .nestedScroll(nestedScrollConnection), + contentPadding = WindowInsets.navigationBars.asPaddingValues(), + ) { + items( + items = list, + contentType = { + when (it) { + is UiModel.Header -> "header" + is UiModel.Item -> "item" + } + }, + key = { + when (it) { + is UiModel.Header -> it.hashCode() + is UiModel.Item -> it.source.key() + } + } + ) { model -> + when (model) { + is UiModel.Header -> { + SourceHeader( + modifier = Modifier.animateItemPlacement(), + language = model.language, + isCategory = model.isCategory + ) + } + is UiModel.Item -> SourceItem( + modifier = Modifier.animateItemPlacement(), + item = model.source, + showLatest = showLatest, + showPin = showPin, + onClickItem = onClickItem, + onLongClickItem = { + setSourceState(it) + }, + onClickLatest = onClickLatest, + onClickPin = onClickPin, + ) + } + } + } + + if (sourceState != null) { + SourceOptionsDialog( + source = sourceState, + onClickPin = { + onClickPin(sourceState) + setSourceState(null) + }, + onClickDisable = { + onClickDisable(sourceState) + setSourceState(null) + }, + onClickSetCategories = { + setSourceCategoriesState(sourceState) + setSourceState(null) + }, + onClickToggleDataSaver = { + onClickToggleDataSaver(sourceState) + setSourceState(null) + }, + onDismiss = { setSourceState(null) } + ) + } + if (sourceCategoriesState != null) { + SourceCategoriesDialog( + source = sourceCategoriesState, + categories = categories, + oldCategories = sourceCategoriesState.categories, + onClickCategories = { + onClickSetCategories(sourceCategoriesState, it) + setSourceCategoriesState(null) + }, + onDismiss = { setSourceCategoriesState(null) }, + ) + } +} + +@Composable +fun SourceHeader( + modifier: Modifier = Modifier, + language: String, + isCategory: Boolean, +) { + val context = LocalContext.current + Text( + text = if (!isCategory) { + LocaleHelper.getSourceDisplayName(language, context) + } else language, + modifier = modifier + .padding(horizontal = horizontalPadding, vertical = 8.dp), + style = MaterialTheme.typography.header, + ) +} + +@Composable +fun SourceItem( + modifier: Modifier = Modifier, + item: Source, + // SY --> + showLatest: Boolean, + showPin: Boolean, + // SY <-- + onClickItem: (Source) -> Unit, + onLongClickItem: (Source) -> Unit, + onClickLatest: (Source) -> Unit, + onClickPin: (Source) -> Unit +) { + Row( + modifier = modifier + .combinedClickable( + onClick = { onClickItem(item) }, + onLongClick = { onLongClickItem(item) } + ) + .padding(horizontal = horizontalPadding, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SourceIcon(source = item) + Column( + modifier = Modifier + .padding(horizontal = horizontalPadding) + .weight(1f) + ) { + Text( + text = item.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = LocaleHelper.getDisplayName(item.lang), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall + ) + } + if (item.supportsLatest /* SY --> */ && showLatest /* SY <-- */) { + TextButton(onClick = { onClickLatest(item) }) { + Text(text = stringResource(id = R.string.latest)) + } + } + + // SY --> + if (showPin) { + SourcePinButton( + isPinned = Pin.Pinned in item.pin, + onClick = { onClickPin(item) }, + ) + } + // SY <-- + } +} + +@Composable +fun SourceIcon( + source: Source +) { + val icon = source.icon + val modifier = Modifier + .height(40.dp) + .aspectRatio(1f) + if (icon != null) { + Image( + bitmap = icon, + contentDescription = "", + modifier = modifier, + ) + } else { + Image( + painter = painterResource(id = R.mipmap.ic_local_source), + contentDescription = "", + modifier = modifier, + ) + } +} + +@Composable +fun SourcePinButton( + isPinned: Boolean, + onClick: () -> Unit +) { + val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin + val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground + IconButton(onClick = onClick) { + Icon( + imageVector = icon, + contentDescription = "", + tint = tint + ) + } +} + +@Composable +fun SourceOptionsDialog( + source: Source, + onClickPin: () -> Unit, + onClickDisable: () -> Unit, + onClickSetCategories: () -> Unit, + onClickToggleDataSaver: () -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + title = { + Text(text = source.nameWithLanguage) + }, + text = { + Column { + val textId = if (Pin.Pinned in source.pin) R.string.action_unpin else R.string.action_pin + Text( + text = stringResource(id = textId), + modifier = Modifier + .clickable(onClick = onClickPin) + .fillMaxWidth() + .padding(vertical = 16.dp) + ) + if (source.id != LocalSource.ID) { + Text( + text = stringResource(id = R.string.action_disable), + modifier = Modifier + .clickable(onClick = onClickDisable) + .fillMaxWidth() + .padding(vertical = 16.dp) + ) + } + // SY --> + Text( + text = stringResource(id = R.string.categories), + modifier = Modifier + .clickable(onClick = onClickSetCategories) + .fillMaxWidth() + .padding(vertical = 16.dp) + ) + Text( + text = if (source.isExcludedFromDataSaver) { + stringResource(id = R.string.data_saver_stop_exclude) + } else { + stringResource(id = R.string.data_saver_exclude) + }, + modifier = Modifier + .clickable(onClick = onClickToggleDataSaver) + .fillMaxWidth() + .padding(vertical = 16.dp), + ) + // SY <-- + } + }, + onDismissRequest = onDismiss, + confirmButton = {}, + ) +} + +// SY --> +@Composable +fun SourceCategoriesDialog( + source: Source, + categories: List, + oldCategories: Set, + onClickCategories: (List) -> Unit, + onDismiss: () -> Unit, +) { + val newCategories = remember { + mutableStateListOf().also { it.addAll(oldCategories) } + } + AlertDialog( + title = { + Text(text = source.nameWithLanguage) + }, + text = { + Column { + categories.forEach { + PreferenceRow( + title = it, + onClick = { + if (it in newCategories) { + newCategories -= it + } else { + newCategories += it + } + }, + action = { + Checkbox(checked = it in newCategories, onCheckedChange = null) + }, + ) + } + } + }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { onClickCategories(newCategories.toList()) }) { + Text(text = stringResource(android.R.string.ok)) + } + }, + ) +} +// SY <-- diff --git a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt index adb6644d2..1ce2b579e 100644 --- a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt +++ b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt @@ -9,7 +9,8 @@ import com.google.android.material.composethemeadapter3.createMdc3Theme fun TachiyomiTheme(content: @Composable () -> Unit) { val context = LocalContext.current val (colorScheme, typography) = createMdc3Theme( - context = context + context = context, + setTextColors = true ) MaterialTheme( diff --git a/app/src/main/java/eu/kanade/presentation/theme/Typography.kt b/app/src/main/java/eu/kanade/presentation/theme/Typography.kt new file mode 100644 index 000000000..74bd03fdf --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/theme/Typography.kt @@ -0,0 +1,16 @@ +package eu.kanade.presentation.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight + +val Typography.header: TextStyle + @Composable + get() { + return bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold + ) + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 4ef5fb86a..0648befdd 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -335,6 +335,8 @@ class PreferencesHelper(val context: Context) { fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false) + fun pinsOnTop() = flowPrefs.getBoolean("pins_on_top", true) + fun setChapterSettingsDefault(manga: Manga) { prefs.edit { putInt(Keys.defaultChapterFilterByRead, manga.readFilter) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 711dada8d..02ec0d10b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -72,13 +72,17 @@ class ExtensionManager( } fun getAppIconForSource(source: Source): Drawable? { - val pkgName = installedExtensions.find { ext -> ext.sources.any { it.id == source.id } }?.pkgName + return getAppIconForSource(source.id) + } + + fun getAppIconForSource(sourceId: Long): Drawable? { + val pkgName = installedExtensions.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName if (pkgName != null) { return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) } } // SY --> - return when (source.id) { + return when (sourceId) { EH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source) EXH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source) MERGED_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_merged_source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index ff357aa1c..903867e61 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -33,9 +33,12 @@ import exh.source.handleSourceLibrary import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import rx.Observable import uy.kohesive.injekt.injectLazy import kotlin.reflect.KClass @@ -45,6 +48,9 @@ open class SourceManager(private val context: Context) { private val sourcesMap = mutableMapOf() private val stubSourcesMap = mutableMapOf() + private val _catalogueSources: MutableStateFlow> = MutableStateFlow(listOf()) + val catalogueSources: Flow> = _catalogueSources + // SY --> private val prefs: PreferencesHelper by injectLazy() @@ -137,15 +143,23 @@ open class SourceManager(private val context: Context) { if (!sourcesMap.containsKey(source.id)) { sourcesMap[source.id] = newSource } + triggerCatalogueSources() } internal fun unregisterSource(source: Source) { sourcesMap.remove(source.id) + triggerCatalogueSources() // SY --> currentDelegatedSources.remove(source.id) // SY <-- } + private fun triggerCatalogueSources() { + _catalogueSources.update { + sourcesMap.values.filterIsInstance() + } + } + private fun createInternalSources(): List = listOf( LocalSource(context), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt index 8e2c9ed2e..e43e5de41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.base.controller +import android.os.Bundle import android.view.LayoutInflater import android.view.View import androidx.compose.runtime.Composable @@ -7,6 +8,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.databinding.ComposeControllerBinding +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import nucleus.presenter.Presenter /** @@ -34,7 +36,26 @@ abstract class ComposeController

> : NucleusController() { +abstract class BasicComposeController(bundle: Bundle? = null) : BaseController(bundle) { + + override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding = + ComposeControllerBinding.inflate(inflater) + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + binding.root.setContent { + val nestedScrollInterop = rememberNestedScrollInteropConnection(binding.root) + TachiyomiTheme { + ComposeContent(nestedScrollInterop) + } + } + } + + @Composable abstract fun ComposeContent(nestedScrollInterop: NestedScrollConnection) +} + +abstract class SearchableComposeController

>(bundle: Bundle? = null) : SearchableNucleusController(bundle) { override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding = ComposeControllerBinding.inflate(inflater) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/LangHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/LangHolder.kt deleted file mode 100644 index 2a7ce220e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/LangHolder.kt +++ /dev/null @@ -1,17 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source - -import android.view.View -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding -import eu.kanade.tachiyomi.util.system.LocaleHelper - -class LangHolder(view: View, adapter: FlexibleAdapter<*>) : - FlexibleViewHolder(view, adapter) { - - private val binding = SectionHeaderItemBinding.bind(view) - - fun bind(item: LangItem) { - binding.title.text = LocaleHelper.getSourceDisplayName(item.code, itemView.context) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/LangItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/LangItem.kt deleted file mode 100644 index a9d02b014..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/LangItem.kt +++ /dev/null @@ -1,42 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractHeaderItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R - -/** - * Item that contains the language header. - * - * @param code The lang code. - */ -data class LangItem(val code: String) : AbstractHeaderItem() { - - /** - * Returns the layout resource of this item. - */ - override fun getLayoutRes(): Int { - return R.layout.section_header_item - } - - /** - * Creates a new view holder for this item. - */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): LangHolder { - return LangHolder(view, adapter) - } - - /** - * Binds this item to the given view holder. - */ - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: LangHolder, - position: Int, - payloads: MutableList, - ) { - holder.bind(this) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceAdapter.kt deleted file mode 100644 index 66d3e7572..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceAdapter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible - -/** - * Adapter that holds the catalogue cards. - * - * @param controller instance of [SourceController]. - */ -class SourceAdapter(controller: SourceController) : - FlexibleAdapter>(null, controller, true) { - - init { - setDisplayHeadersAtStartUp(true) - } - - /** - * Listener for browse item clicks. - */ - val clickListener: OnSourceClickListener = controller - - /** - * Listener which should be called when user clicks browse. - * Note: Should only be handled by [SourceController] - */ - interface OnSourceClickListener { - fun onBrowseClick(position: Int) - fun onLatestClick(position: Int) - fun onPinClick(position: Int) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index ac20b4927..f26a0eb9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt @@ -1,65 +1,39 @@ package eu.kanade.tachiyomi.ui.browse.source import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.app.Dialog import android.os.Bundle import android.os.Parcelable -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import androidx.core.os.bundleOf -import androidx.recyclerview.widget.LinearLayoutManager -import com.bluelinelabs.conductor.ControllerChangeHandler -import com.bluelinelabs.conductor.ControllerChangeType -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import com.bluelinelabs.conductor.Controller +import eu.kanade.domain.source.model.Source +import eu.kanade.presentation.source.SourceScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController +import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe -import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController -import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.preference.minusAssign -import eu.kanade.tachiyomi.util.preference.plusAssign -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.onAnimationsFinished -import exh.ui.smartsearch.SmartSearchController import kotlinx.parcelize.Parcelize import uy.kohesive.injekt.injectLazy /** * This controller shows and manages the different catalogues enabled by the user. * This controller should only handle UI actions, IO actions should be done by [SourcePresenter] - * [SourceAdapter.OnSourceClickListener] call function data on browse item click. - * [SourceAdapter.OnLatestClickListener] call function data on latest item click */ -class SourceController(bundle: Bundle? = null) : - SearchableNucleusController(bundle), - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - SourceAdapter.OnSourceClickListener, - /*SY -->*/ - ChangeSourceCategoriesDialog.Listener /*SY <--*/ { +class SourceController(bundle: Bundle? = null) : SearchableComposeController(bundle) { private val preferences: PreferencesHelper by injectLazy() - private var adapter: SourceAdapter? = null - // EXH --> private val smartSearchConfig: SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG) @@ -81,241 +55,57 @@ class SourceController(bundle: Bundle? = null) : // SY <-- } - override fun createPresenter(): SourcePresenter { - return SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */) - } + override fun createPresenter(): SourcePresenter = + SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */) - override fun createBinding(inflater: LayoutInflater) = SourceMainControllerBinding.inflate(inflater) + @Composable + override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { + SourceScreen( + nestedScrollInterop = nestedScrollInterop, + presenter = presenter, + onClickItem = { source -> + if (preferences.useNewSourceNavigation().get()) { + openSource(source, SourceFeedController(source.id)) + } else { + openSource(source, BrowseSourceController(source)) + } + }, + onClickDisable = { source -> + presenter.disableSource(source) + }, + onClickLatest = { source -> + openSource(source, LatestUpdatesController(source)) + }, + onClickPin = { source -> + presenter.togglePin(source) + }, + onClickSetCategories = { source, categories -> + presenter.setSourceCategories(source, categories) + }, + onClickToggleDataSaver = { source -> + presenter.toggleExcludeFromDataSaver(source) + }, + ) + LaunchedEffect(Unit) { + (activity as? MainActivity)?.ready = true + } + } override fun onViewCreated(view: View) { super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - adapter = SourceAdapter(this) - - // Create recycler and set adapter. - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.adapter = adapter - binding.recycler.onAnimationsFinished { - (activity as? MainActivity)?.ready = true - } - adapter?.fastScroller = binding.fastScroller - requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) - - // SY --> - if (mode == Mode.CATALOGUE) { - // Update list on extension changes (e.g. new installation) - (parentController as BrowseController).extensionListUpdateRelay - .skip(1) // Skip first update when ExtensionController created - .subscribeUntilDestroy { - presenter.updateSources() - } - } - // SY <-- - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { - super.onChangeStarted(handler, type) - if (type.isPush) { - presenter.updateSources() - } - } - - override fun onItemClick(view: View, position: Int): Boolean { - onItemClick(position) - return false - } - - private fun onItemClick(position: Int) { - val item = adapter?.getItem(position) as? SourceItem ?: return - val source = item.source - // SY --> - when (mode) { - Mode.CATALOGUE -> { - // Open the catalogue view. - // SY --> - if (preferences.useNewSourceNavigation().get()) { - openSourceFeed(source) - } else openSource(source, BrowseSourceController(source)) - // SY <-- - } - Mode.SMART_SEARCH -> router.pushController( - SmartSearchController( - bundleOf( - SmartSearchController.ARG_SOURCE_ID to source.id, - SmartSearchController.ARG_SMART_SEARCH_CONFIG to smartSearchConfig, - ), - ), - ) - } - // SY <-- - } - - override fun onItemLongClick(position: Int) { - val activity = activity ?: return - val item = adapter?.getItem(position) as? SourceItem ?: return - - val isPinned = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false - - val items = mutableListOf( - activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin) to { toggleSourcePin(item.source) }, - ) - if (item.source !is LocalSource) { - items.add(activity.getString(R.string.action_disable) to { disableSource(item.source) }) - } - - // SY --> - items.add( - activity.getString(R.string.categories) to { addToCategories(item.source) }, - ) - - if (preferences.dataSaver().get()) { - val isExcluded = item.source.id.toString() in preferences.dataSaverExcludedSources().get() - items.add( - activity.getString( - if (isExcluded) R.string.data_saver_stop_exclude else R.string.data_saver_exclude, - ) to { - excludeFromDataSaver(item.source, isExcluded) - }, - ) - } - // SY <-- - - SourceOptionsDialog(item.source.toString(), items).showDialog(router) - } - - private fun disableSource(source: Source) { - preferences.disabledSources() += source.id.toString() - - presenter.updateSources() - } - - private fun toggleSourcePin(source: Source) { - val isPinned = source.id.toString() in preferences.pinnedSources().get() - if (isPinned) { - preferences.pinnedSources() -= source.id.toString() - } else { - preferences.pinnedSources() += source.id.toString() - } - - presenter.updateSources() - } - - // SY --> - private fun addToCategories(source: Source) { - val categories = preferences.sourcesTabCategories().get() - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it })) - .toTypedArray() - - if (categories.isEmpty()) { - applicationContext?.toast(R.string.no_source_categories) - return - } - - val preferenceSources = preferences.sourcesTabSourcesInCategories().get().toMutableList() - val sources = preferenceSources.map { it.split("|")[0] } - - if (source.id.toString() in sources) { - val sourceCategories = preferenceSources - .map { item -> item.split("|").let { it.component1() to it.component2() } } - .filter { it.first == source.id.toString() } - .map { it.second } - - val selection = categories.map { it in sourceCategories } - .toBooleanArray() - - ChangeSourceCategoriesDialog(this, source, categories, selection) - .showDialog(router) - } else { - ChangeSourceCategoriesDialog(this, source, categories, categories.map { false }.toBooleanArray()) - .showDialog(router) - } - } - - override fun updateCategoriesForSource(source: Source, categories: List) { - var preferenceSources = preferences.sourcesTabSourcesInCategories().get().toMutableList() - val sources = preferenceSources.map { it.split("|")[0] } - - if (source.id.toString() in sources) { - preferenceSources = preferenceSources - .map { it.split("|") } - .filter { it[0] != source.id.toString() } - .map { it[0] + "|" + it[1] }.toMutableList() - } - - categories.forEach { - preferenceSources.add(source.id.toString() + "|" + it) - } - - preferences.sourcesTabSourcesInCategories().set( - preferenceSources.sorted().toSet(), - ) - presenter.updateSources() - } - - private fun excludeFromDataSaver(source: Source, isExcluded: Boolean) { - if (isExcluded) { - preferences.dataSaverExcludedSources() -= source.id.toString() - } else { - preferences.dataSaverExcludedSources() += source.id.toString() - } - } - // SY <-- - - /** - * Called when browse is clicked in [SourceAdapter] - */ - override fun onBrowseClick(position: Int) { - onItemClick(position) - } - - /** - * Called when latest is clicked in [SourceAdapter] - */ - override fun onLatestClick(position: Int) { - val item = adapter?.getItem(position) as? SourceItem ?: return - openSource(item.source, LatestUpdatesController(item.source)) - } - - /** - * Called when pin icon is clicked in [SourceAdapter] - */ - override fun onPinClick(position: Int) { - val item = adapter?.getItem(position) as? SourceItem ?: return - toggleSourcePin(item.source) } /** * Opens a catalogue with the given controller. */ - private fun openSource(source: CatalogueSource, controller: BrowseSourceController) { + private fun openSource(source: Source, controller: Controller) { if (!preferences.incognitoMode().get()) { preferences.lastUsedSource().set(source.id) } parentController!!.router.pushController(controller) } - // SY --> - /** - * Opens a catalogue with the source feed controller. - */ - private fun openSourceFeed(source: CatalogueSource) { - preferences.lastUsedSource().set(source.id) - parentController!!.router.pushController(SourceFeedController(source)) - } - // SY <-- - /** * Called when an option menu item has been selected by the user. * @@ -323,51 +113,13 @@ class SourceController(bundle: Bundle? = null) : * @return True if this event has been consumed, false if it has not. */ override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + return when (item.itemId) { // Initialize option to open catalogue settings. R.id.action_settings -> { parentController!!.router.pushController(SourceFilterController()) + true } - } - return super.onOptionsItemSelected(item) - } - - /** - * Called to update adapter containing sources. - */ - fun setSources(sources: List>) { - adapter?.updateDataSet(sources) - } - - /** - * Called to set the last used catalogue at the top of the view. - */ - fun setLastUsedSource(item: SourceItem?) { - adapter?.removeAllScrollableHeaders() - if (item != null) { - adapter?.addScrollableHeader(item) - adapter?.addScrollableHeader(LangItem(SourcePresenter.LAST_USED_KEY)) - } - } - - class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) { - - private lateinit var source: String - private lateinit var items: List Unit>> - - constructor(source: String, items: List Unit>>) : this() { - this.source = source - this.items = items - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(source) - .setItems(items.map { it.first }.toTypedArray()) { dialog, which -> - items[which].second() - dialog.dismiss() - } - .create() + else -> super.onOptionsItemSelected(item) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt deleted file mode 100644 index cc7e451bf..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt +++ /dev/null @@ -1,58 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source - -import android.view.View -import androidx.core.view.isVisible -import coil.load -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.SourceMainControllerItemBinding -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.icon -import eu.kanade.tachiyomi.util.system.LocaleHelper -import eu.kanade.tachiyomi.util.view.setVectorCompat - -class SourceHolder(view: View, val adapter: SourceAdapter /* SY --> */, private val showLatest: Boolean, private val showPins: Boolean /* SY <-- */) : - FlexibleViewHolder(view, adapter) { - - private val binding = SourceMainControllerItemBinding.bind(view) - - init { - binding.sourceLatest.setOnClickListener { - adapter.clickListener.onLatestClick(bindingAdapterPosition) - } - - binding.pin.setOnClickListener { - adapter.clickListener.onPinClick(bindingAdapterPosition) - } - - // SY --> - if (!showLatest) { - binding.sourceLatest.isVisible = false - } - // SY <-- - } - - fun bind(item: SourceItem) { - val source = item.source - - binding.title.text = source.name - binding.subtitle.isVisible = source !is LocalSource - binding.subtitle.text = LocaleHelper.getDisplayName(source.lang) - - // Set source icon - val icon = source.icon() - when { - icon != null -> binding.image.load(icon) - item.source.id == LocalSource.ID -> binding.image.load(R.mipmap.ic_local_source) - } - - binding.sourceLatest.isVisible = source.supportsLatest/* SY --> */ && showLatest // SY <-- - - binding.pin.isVisible = showPins - if (item.isPinned) { - binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, R.attr.colorAccent) - } else { - binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, android.R.attr.textColorHint) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt deleted file mode 100644 index 0a21b538d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt +++ /dev/null @@ -1,60 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractSectionableItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.CatalogueSource - -/** - * Item that contains source information. - * - * @param source Instance of [CatalogueSource] containing source information. - * @param header The header for this item. - */ -data class SourceItem( - val source: CatalogueSource, - val header: LangItem? = null, - val isPinned: Boolean = false, - // SY --> - val showLatest: Boolean, - val showPins: Boolean, - // SY <-- -) : - AbstractSectionableItem(header) { - - override fun getLayoutRes(): Int { - return R.layout.source_main_controller_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): SourceHolder { - return SourceHolder(view, adapter as SourceAdapter /* SY --> */, showLatest, showPins /* SY <-- */) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: SourceHolder, - position: Int, - payloads: MutableList, - ) { - holder.bind(this) - } - - override fun equals(other: Any?): Boolean { - if (other is SourceItem) { - return source.id == other.source.id && - getHeader()?.code == other.getHeader()?.code && - isPinned == other.isPinned - } - return false - } - - override fun hashCode(): Int { - var result = source.id.hashCode() - result = 31 * result + (header?.hashCode() ?: 0) - result = 31 * result + isPinned.hashCode() - return result - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt index d2898ba5a..18d63349d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt @@ -1,16 +1,23 @@ package eu.kanade.tachiyomi.ui.browse.source -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.source.SourceManager +import android.os.Bundle +import eu.kanade.domain.source.interactor.DisableSource +import eu.kanade.domain.source.interactor.GetEnabledSources +import eu.kanade.domain.source.interactor.GetShowLatest +import eu.kanade.domain.source.interactor.GetSourceCategories +import eu.kanade.domain.source.interactor.SetSourceCategories +import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver +import eu.kanade.domain.source.interactor.ToggleSourcePin +import eu.kanade.domain.source.model.Pin +import eu.kanade.domain.source.model.Source import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart +import eu.kanade.tachiyomi.util.lang.launchIO +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.TreeMap @@ -20,146 +27,130 @@ import java.util.TreeMap * Function calls should be done from here. UI calls should be done from the controller. */ class SourcePresenter( - val sourceManager: SourceManager = Injekt.get(), - private val preferences: PreferencesHelper = Injekt.get(), + private val getEnabledSources: GetEnabledSources = Injekt.get(), + private val disableSource: DisableSource = Injekt.get(), + private val toggleSourcePin: ToggleSourcePin = Injekt.get(), // SY --> + private val getSourceCategories: GetSourceCategories = Injekt.get(), + private val getShowLatest: GetShowLatest = Injekt.get(), + private val toggleExcludeFromDataSaver: ToggleExcludeFromDataSaver = Injekt.get(), + private val setSourceCategories: SetSourceCategories = Injekt.get(), private val controllerMode: SourceController.Mode, // SY <-- ) : BasePresenter() { - var sources = getEnabledSources() - - /** - * Unsubscribe and create a new subscription to fetch enabled sources. - */ - private fun loadSources() { - val pinnedSources = mutableListOf() - val pinnedSourceIds = preferences.pinnedSources().get() + private val _state: MutableStateFlow = MutableStateFlow(SourceState.EMPTY) + val state: StateFlow = _state.asStateFlow() + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + presenterScope.launchIO { + getEnabledSources.subscribe() + .catch { exception -> + _state.update { state -> + state.copy(sources = listOf(), error = exception) + } + } + .collectLatest(::collectLatestSources) + } // SY --> - val categories = preferences.sourcesTabCategories().get() - .sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it })) - .map { - SourceCategory(it) + presenterScope.launchIO { + getSourceCategories.subscribe() + .catch { exception -> + _state.update { state -> + state.copy(sources = listOf(), error = exception) + } + } + .collectLatest(::updateCategories) + } + presenterScope.launchIO { + _state.update { state -> + state.copy( + showPin = controllerMode == SourceController.Mode.CATALOGUE, + ) } - - val sourcesAndCategoriesCombined = preferences.sourcesTabSourcesInCategories().get() - val sourcesAndCategories = if (sourcesAndCategoriesCombined.isNotEmpty()) sourcesAndCategoriesCombined.map { - val temp = it.split("|") - temp[0] to temp[1] - } else null - - val sourcesInCategories = sourcesAndCategories?.map { it.first } + } + presenterScope.launchIO { + getShowLatest.subscribe(mode = controllerMode) + .catch { exception -> + _state.update { state -> + state.copy(sources = listOf(), error = exception) + } + } + .collectLatest(::updateShowLatest) + } // SY <-- + } - val map = TreeMap> { d1, d2 -> + private suspend fun collectLatestSources(sources: List) { + val map = TreeMap> { d1, d2 -> // Catalogues without a lang defined will be placed at the end when { + d1 == LAST_USED_KEY && d2 != LAST_USED_KEY -> -1 + d2 == LAST_USED_KEY && d1 != LAST_USED_KEY -> 1 + d1 == PINNED_KEY && d2 != PINNED_KEY -> -1 + d2 == PINNED_KEY && d1 != PINNED_KEY -> 1 d1 == "" && d2 != "" -> 1 d2 == "" && d1 != "" -> -1 else -> d1.compareTo(d2) } } - val byLang = sources.groupByTo(map) { it.lang } - var sourceItems = byLang.flatMap { - val langItem = LangItem(it.key) - it.value.map { source -> + val byLang = sources.groupByTo(map) { + when { // SY --> - val showPins = controllerMode == SourceController.Mode.CATALOGUE - val showLatest = showPins && !preferences.useNewSourceNavigation().get() + it.category != null -> it.category // SY <-- - val isPinned = source.id.toString() in pinnedSourceIds - if (isPinned) { - pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), isPinned /* SY --> */, showLatest, showPins /* SY <-- */)) - } - - // SY --> - if (sourcesInCategories != null && source.id.toString() in sourcesInCategories) { - sourcesAndCategories - .filter { SourcesAndCategory -> SourcesAndCategory.first == source.id.toString() } - .forEach { SourceAndCategory -> - categories.forEach { dataClass -> - if (dataClass.category.trim() == SourceAndCategory.second.trim()) { - dataClass.sources.add( - SourceItem( - source, - LangItem("custom|" + SourceAndCategory.second), - isPinned, - showLatest, - showPins, - ), - ) - } - } - } - } - // SY <-- - - SourceItem(source, langItem, isPinned /* SY --> */, showLatest, showPins /* SY <-- */) + it.isUsedLast -> LAST_USED_KEY + Pin.Actual in it.pin -> PINNED_KEY + else -> it.lang } } - - if (preferences.sourcesTabCategoriesFilter().get()) { - sourcesInCategories?.let { sourcesIds -> sourceItems = sourceItems.filterNot { it.source.id.toString() in sourcesIds } } + _state.update { state -> + state.copy( + sources = byLang.flatMap { + listOf( + UiModel.Header(it.key, it.value.firstOrNull()?.category != null), + *it.value.map { source -> + UiModel.Item(source) + }.toTypedArray() + ) + }, + error = null + ) } - - // SY --> - categories.forEach { - sourceItems = it.sources.sortedBy { sourceItem -> sourceItem.source.name.lowercase() } + sourceItems - } - // SY <-- - - if (pinnedSources.isNotEmpty()) { - sourceItems = pinnedSources + sourceItems - } - - view?.setSources(sourceItems) } - private fun loadLastUsedSource() { - // Immediate initial load - preferences.lastUsedSource().get().let { updateLastUsedSource(it) } - - // Subsequent updates - preferences.lastUsedSource().asFlow() - .drop(1) - .onStart { delay(500) } - .distinctUntilChanged() - .onEach { updateLastUsedSource(it) } - .launchIn(presenterScope) - } - - private fun updateLastUsedSource(sourceId: Long) { - val source = (sourceManager.get(sourceId) as? CatalogueSource)?.let { - val isPinned = it.id.toString() in preferences.pinnedSources().get() - // SY --> - val showPins = controllerMode == SourceController.Mode.CATALOGUE - val showLatest = showPins && !preferences.useNewSourceNavigation().get() - // SY <-- - SourceItem(it, null, isPinned /* SY --> */, showLatest, showPins /* SY <-- */) + // SY --> + private suspend fun updateCategories(categories: Set) { + _state.update { state -> + state.copy( + sourceCategories = categories.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it }) + ) } - source?.let { view?.setLastUsedSource(it) } + } + private suspend fun updateShowLatest(showLatest: Boolean) { + _state.update { state -> + state.copy( + showLatest = showLatest + ) + } + } + // SY <-- + + fun disableSource(source: Source) { + disableSource.await(source) } - fun updateSources() { - sources = getEnabledSources() - loadSources() - loadLastUsedSource() + fun togglePin(source: Source) { + toggleSourcePin.await(source) } - /** - * Returns a list of enabled sources ordered by language and name. - * - * @return list containing enabled sources. - */ - private fun getEnabledSources(): List { - val languages = preferences.enabledLanguages().get() - val disabledSourceIds = preferences.disabledSources().get() + fun toggleExcludeFromDataSaver(source: Source) { + toggleExcludeFromDataSaver.await(source) + } - return sourceManager.getVisibleCatalogueSources() - .filter { it.lang in languages || it.id == LocalSource.ID } - .filterNot { it.id.toString() in disabledSourceIds } - .sortedBy { "(${it.lang}) ${it.name.lowercase()}" } + fun setSourceCategories(source: Source, categories: List) { + setSourceCategories.await(source, categories) } companion object { @@ -168,6 +159,29 @@ class SourcePresenter( } } -// SY --> -data class SourceCategory(val category: String, var sources: MutableList = mutableListOf()) -// SY <-- +sealed class UiModel { + data class Item(val source: Source) : UiModel() + data class Header(val language: String, val isCategory: Boolean) : UiModel() +} + +data class SourceState( + val sources: List, + val error: Throwable?, + val sourceCategories: List, + val showLatest: Boolean, + val showPin: Boolean +) { + + val isLoading: Boolean + get() = sources.isEmpty() && error == null + + val hasError: Boolean + get() = error != null + + val isEmpty: Boolean + get() = sources.isEmpty() + + companion object { + val EMPTY = SourceState(listOf(), null, emptyList(), true, true) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index 5729c61eb..1d8198497 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -20,6 +20,7 @@ import com.google.android.material.snackbar.Snackbar import dev.chrisbanes.insetter.applyInsetter import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.domain.source.model.Source import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga @@ -80,8 +81,8 @@ open class BrowseSourceController(bundle: Bundle) : ChangeMangaCategoriesDialog.Listener { constructor( - source: CatalogueSource, - searchQuery: String? = null, + sourceId: Long, + query: String? = null, // SY --> smartSearchConfig: SourceController.SmartSearchConfig? = null, savedSearch: Long? = null, @@ -89,10 +90,9 @@ open class BrowseSourceController(bundle: Bundle) : // SY <-- ) : this( Bundle().apply { - putLong(SOURCE_ID_KEY, source.id) - - if (searchQuery != null) { - putString(SEARCH_QUERY_KEY, searchQuery) + putLong(SOURCE_ID_KEY, sourceId) + if (query != null) { + putString(SEARCH_QUERY_KEY, query) } // SY --> @@ -111,6 +111,38 @@ open class BrowseSourceController(bundle: Bundle) : }, ) + constructor( + source: CatalogueSource, + query: String? = null, + // SY --> + smartSearchConfig: SourceController.SmartSearchConfig? = null, + savedSearch: Long? = null, + filterList: String? = null, + // SY <-- + ) : this( + source.id, + query, + smartSearchConfig, + savedSearch, + filterList + ) + + constructor( + source: Source, + query: String? = null, + // SY --> + smartSearchConfig: SourceController.SmartSearchConfig? = null, + savedSearch: Long? = null, + filterList: String? = null, + // SY <-- + ) : this( + source.id, + query, + smartSearchConfig, + savedSearch, + filterList + ) + private val preferences: PreferencesHelper by injectLazy() /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt index 2561eeb57..602e52585 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.source.latest import android.os.Bundle import android.view.Menu import androidx.core.os.bundleOf +import eu.kanade.domain.source.model.Source import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController @@ -13,9 +14,15 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter */ class LatestUpdatesController(bundle: Bundle) : BrowseSourceController(bundle) { + constructor(source: Source) : this( + bundleOf(SOURCE_ID_KEY to source.id), + ) + + // SY --> constructor(source: CatalogueSource) : this( bundleOf(SOURCE_ID_KEY to source.id), ) + // SY <-- override fun createPresenter(): BrowseSourcePresenter { return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt index 8146d221b..051d82aed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt @@ -69,6 +69,17 @@ class SettingsBrowseController : SettingsController() { } // SY <-- + preferenceCategory { + titleRes = R.string.pref_category_general + + switchPreference { + bindTo(preferences.pinsOnTop()) + titleRes = R.string.pref_move_on_top + summaryRes = R.string.pref_move_on_top_summary + defaultValue = true + } + } + preferenceCategory { titleRes = R.string.label_extensions diff --git a/app/src/main/res/drawable/ic_push_pin_24dp.xml b/app/src/main/res/drawable/ic_push_pin_24dp.xml deleted file mode 100644 index 8bc324eed..000000000 --- a/app/src/main/res/drawable/ic_push_pin_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_push_pin_outline_24dp.xml b/app/src/main/res/drawable/ic_push_pin_outline_24dp.xml deleted file mode 100644 index e481d37d6..000000000 --- a/app/src/main/res/drawable/ic_push_pin_outline_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/source_main_controller.xml b/app/src/main/res/layout/source_main_controller.xml deleted file mode 100644 index 64af2528c..000000000 --- a/app/src/main/res/layout/source_main_controller.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/source_main_controller_item.xml b/app/src/main/res/layout/source_main_controller_item.xml index c86b09598..f3b94ae0c 100644 --- a/app/src/main/res/layout/source_main_controller_item.xml +++ b/app/src/main/res/layout/source_main_controller_item.xml @@ -39,6 +39,7 @@ android:id="@+id/subtitle" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginEnd="8dp" android:maxLines="1" android:textAppearance="?attr/textAppearanceBodySmall" android:visibility="gone" @@ -49,11 +50,13 @@ tools:text="English" tools:visibility="visible" /> +