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
This commit is contained in:
parent
41b2c948e1
commit
c1659ad908
13
app/src/main/java/eu/kanade/data/source/SourceMapper.kt
Normal file
13
app/src/main/java/eu/kanade/data/source/SourceMapper.kt
Normal file
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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<List<Source>> {
|
||||||
|
return sourceManager.catalogueSources.map { sources ->
|
||||||
|
sources.map(sourceMapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,21 @@
|
|||||||
package eu.kanade.domain
|
package eu.kanade.domain
|
||||||
|
|
||||||
import eu.kanade.data.history.HistoryRepositoryImpl
|
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.DeleteHistoryTable
|
||||||
import eu.kanade.domain.history.interactor.GetHistory
|
import eu.kanade.domain.history.interactor.GetHistory
|
||||||
import eu.kanade.domain.history.interactor.GetNextChapterForManga
|
import eu.kanade.domain.history.interactor.GetNextChapterForManga
|
||||||
import eu.kanade.domain.history.interactor.RemoveHistoryById
|
import eu.kanade.domain.history.interactor.RemoveHistoryById
|
||||||
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
|
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
|
||||||
import eu.kanade.domain.history.repository.HistoryRepository
|
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.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addFactory
|
import uy.kohesive.injekt.api.addFactory
|
||||||
@ -22,5 +31,16 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetNextChapterForManga(get()) }
|
addFactory { GetNextChapterForManga(get()) }
|
||||||
addFactory { RemoveHistoryById(get()) }
|
addFactory { RemoveHistoryById(get()) }
|
||||||
addFactory { RemoveHistoryByMangaId(get()) }
|
addFactory { RemoveHistoryByMangaId(get()) }
|
||||||
|
addSingletonFactory<SourceRepository> { 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 <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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<List<Source>> {
|
||||||
|
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<String> = setOf(),
|
||||||
|
val enabledSources: Set<String> = setOf(),
|
||||||
|
val disabledSources: Set<String> = setOf(),
|
||||||
|
val lastUsedSource: Long? = null,
|
||||||
|
// SY -->
|
||||||
|
val excludedFromDataSaver: Set<String> = setOf(),
|
||||||
|
val sourcesInCategories: Set<String> = setOf(),
|
||||||
|
val sourceCategoriesFilter: Boolean = false,
|
||||||
|
// SY <--
|
||||||
|
)
|
@ -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<Boolean> {
|
||||||
|
return preferences.useNewSourceNavigation().asFlow()
|
||||||
|
.map {
|
||||||
|
mode == SourceController.Mode.CATALOGUE && !it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<Set<String>> {
|
||||||
|
return preferences.sourcesTabCategories().asFlow()
|
||||||
|
}
|
||||||
|
}
|
@ -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<String>) {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
app/src/main/java/eu/kanade/domain/source/model/Source.kt
Normal file
84
app/src/main/java/eu/kanade/domain/source/model/Source.kt
Normal file
@ -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<String> = emptySet(),
|
||||||
|
// SY <--
|
||||||
|
) {
|
||||||
|
|
||||||
|
val nameWithLanguage: String
|
||||||
|
get() = "$name (${lang.uppercase()})"
|
||||||
|
|
||||||
|
val icon: ImageBitmap?
|
||||||
|
get() {
|
||||||
|
return Injekt.get<ExtensionManager>().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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<List<Source>>
|
||||||
|
}
|
402
app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt
Normal file
402
app/src/main/java/eu/kanade/presentation/source/SourceScreen.kt
Normal file
@ -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<String>) -> 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<UiModel>,
|
||||||
|
categories: List<String>,
|
||||||
|
showPin: Boolean,
|
||||||
|
showLatest: Boolean,
|
||||||
|
onClickItem: (Source) -> Unit,
|
||||||
|
onClickDisable: (Source) -> Unit,
|
||||||
|
onClickLatest: (Source) -> Unit,
|
||||||
|
onClickPin: (Source) -> Unit,
|
||||||
|
onClickSetCategories: (Source, List<String>) -> Unit,
|
||||||
|
onClickToggleDataSaver: (Source) -> Unit
|
||||||
|
) {
|
||||||
|
val (sourceState, setSourceState) = remember { mutableStateOf<Source?>(null) }
|
||||||
|
// SY -->
|
||||||
|
val (sourceCategoriesState, setSourceCategoriesState) = remember { mutableStateOf<Source?>(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<String>,
|
||||||
|
oldCategories: Set<String>,
|
||||||
|
onClickCategories: (List<String>) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val newCategories = remember {
|
||||||
|
mutableStateListOf<String>().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 <--
|
@ -9,7 +9,8 @@ import com.google.android.material.composethemeadapter3.createMdc3Theme
|
|||||||
fun TachiyomiTheme(content: @Composable () -> Unit) {
|
fun TachiyomiTheme(content: @Composable () -> Unit) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val (colorScheme, typography) = createMdc3Theme(
|
val (colorScheme, typography) = createMdc3Theme(
|
||||||
context = context
|
context = context,
|
||||||
|
setTextColors = true
|
||||||
)
|
)
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
|
16
app/src/main/java/eu/kanade/presentation/theme/Typography.kt
Normal file
16
app/src/main/java/eu/kanade/presentation/theme/Typography.kt
Normal file
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -335,6 +335,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
||||||
|
|
||||||
|
fun pinsOnTop() = flowPrefs.getBoolean("pins_on_top", true)
|
||||||
|
|
||||||
fun setChapterSettingsDefault(manga: Manga) {
|
fun setChapterSettingsDefault(manga: Manga) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
||||||
|
@ -72,13 +72,17 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAppIconForSource(source: Source): Drawable? {
|
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) {
|
if (pkgName != null) {
|
||||||
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
|
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
return when (source.id) {
|
return when (sourceId) {
|
||||||
EH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
|
EH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
|
||||||
EXH_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)
|
MERGED_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_merged_source)
|
||||||
|
@ -33,9 +33,12 @@ import exh.source.handleSourceLibrary
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
@ -45,6 +48,9 @@ open class SourceManager(private val context: Context) {
|
|||||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||||
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
||||||
|
|
||||||
|
private val _catalogueSources: MutableStateFlow<List<CatalogueSource>> = MutableStateFlow(listOf())
|
||||||
|
val catalogueSources: Flow<List<CatalogueSource>> = _catalogueSources
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private val prefs: PreferencesHelper by injectLazy()
|
private val prefs: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
@ -137,15 +143,23 @@ open class SourceManager(private val context: Context) {
|
|||||||
if (!sourcesMap.containsKey(source.id)) {
|
if (!sourcesMap.containsKey(source.id)) {
|
||||||
sourcesMap[source.id] = newSource
|
sourcesMap[source.id] = newSource
|
||||||
}
|
}
|
||||||
|
triggerCatalogueSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun unregisterSource(source: Source) {
|
internal fun unregisterSource(source: Source) {
|
||||||
sourcesMap.remove(source.id)
|
sourcesMap.remove(source.id)
|
||||||
|
triggerCatalogueSources()
|
||||||
// SY -->
|
// SY -->
|
||||||
currentDelegatedSources.remove(source.id)
|
currentDelegatedSources.remove(source.id)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun triggerCatalogueSources() {
|
||||||
|
_catalogueSources.update {
|
||||||
|
sourcesMap.values.filterIsInstance<CatalogueSource>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createInternalSources(): List<Source> = listOf(
|
private fun createInternalSources(): List<Source> = listOf(
|
||||||
LocalSource(context),
|
LocalSource(context),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.controller
|
package eu.kanade.tachiyomi.ui.base.controller
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -7,6 +8,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import nucleus.presenter.Presenter
|
import nucleus.presenter.Presenter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,7 +36,26 @@ abstract class ComposeController<P : Presenter<*>> : NucleusController<ComposeCo
|
|||||||
/**
|
/**
|
||||||
* Basic Compose controller without a presenter.
|
* Basic Compose controller without a presenter.
|
||||||
*/
|
*/
|
||||||
abstract class BasicComposeController : BaseController<ComposeControllerBinding>() {
|
abstract class BasicComposeController(bundle: Bundle? = null) : BaseController<ComposeControllerBinding>(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<P : BasePresenter<*>>(bundle: Bundle? = null) : SearchableNucleusController<ComposeControllerBinding, P>(bundle) {
|
||||||
|
|
||||||
override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding =
|
override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding =
|
||||||
ComposeControllerBinding.inflate(inflater)
|
ComposeControllerBinding.inflate(inflater)
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<LangHolder>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<IFlexible<RecyclerView.ViewHolder>>): LangHolder {
|
|
||||||
return LangHolder(view, adapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds this item to the given view holder.
|
|
||||||
*/
|
|
||||||
override fun bindViewHolder(
|
|
||||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
|
||||||
holder: LangHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: MutableList<Any>,
|
|
||||||
) {
|
|
||||||
holder.bind(this)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<IFlexible<*>>(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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +1,39 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
package eu.kanade.tachiyomi.ui.browse.source
|
||||||
|
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.os.bundleOf
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import eu.kanade.domain.source.model.Source
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import eu.kanade.presentation.source.SourceScreen
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
|
import eu.kanade.tachiyomi.ui.base.controller.SearchableComposeController
|
||||||
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.pushController
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
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.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedController
|
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.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
|
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.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 kotlinx.parcelize.Parcelize
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This controller shows and manages the different catalogues enabled by the user.
|
* 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]
|
* 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) :
|
class SourceController(bundle: Bundle? = null) : SearchableComposeController<SourcePresenter>(bundle) {
|
||||||
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
|
|
||||||
FlexibleAdapter.OnItemClickListener,
|
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
|
||||||
SourceAdapter.OnSourceClickListener,
|
|
||||||
/*SY -->*/
|
|
||||||
ChangeSourceCategoriesDialog.Listener /*SY <--*/ {
|
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private var adapter: SourceAdapter? = null
|
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
private val smartSearchConfig: SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG)
|
private val smartSearchConfig: SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG)
|
||||||
|
|
||||||
@ -81,241 +55,57 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createPresenter(): SourcePresenter {
|
override fun createPresenter(): SourcePresenter =
|
||||||
return SourcePresenter(/* SY --> */ controllerMode = mode /* SY <-- */)
|
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) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(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)
|
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<String>) {
|
|
||||||
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.
|
* 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()) {
|
if (!preferences.incognitoMode().get()) {
|
||||||
preferences.lastUsedSource().set(source.id)
|
preferences.lastUsedSource().set(source.id)
|
||||||
}
|
}
|
||||||
parentController!!.router.pushController(controller)
|
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.
|
* 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.
|
* @return True if this event has been consumed, false if it has not.
|
||||||
*/
|
*/
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
return when (item.itemId) {
|
||||||
// Initialize option to open catalogue settings.
|
// Initialize option to open catalogue settings.
|
||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
parentController!!.router.pushController(SourceFilterController())
|
parentController!!.router.pushController(SourceFilterController())
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
else -> super.onOptionsItemSelected(item)
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update adapter containing sources.
|
|
||||||
*/
|
|
||||||
fun setSources(sources: List<IFlexible<*>>) {
|
|
||||||
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<Pair<String, () -> Unit>>
|
|
||||||
|
|
||||||
constructor(source: String, items: List<Pair<String, () -> 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<SourceHolder, LangItem>(header) {
|
|
||||||
|
|
||||||
override fun getLayoutRes(): Int {
|
|
||||||
return R.layout.source_main_controller_item
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
|
||||||
return SourceHolder(view, adapter as SourceAdapter /* SY --> */, showLatest, showPins /* SY <-- */)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bindViewHolder(
|
|
||||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
|
||||||
holder: SourceHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: MutableList<Any>,
|
|
||||||
) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source
|
package eu.kanade.tachiyomi.ui.browse.source
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.domain.source.interactor.DisableSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
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 eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import kotlinx.coroutines.delay
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.TreeMap
|
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.
|
* Function calls should be done from here. UI calls should be done from the controller.
|
||||||
*/
|
*/
|
||||||
class SourcePresenter(
|
class SourcePresenter(
|
||||||
val sourceManager: SourceManager = Injekt.get(),
|
private val getEnabledSources: GetEnabledSources = Injekt.get(),
|
||||||
private val preferences: PreferencesHelper = Injekt.get(),
|
private val disableSource: DisableSource = Injekt.get(),
|
||||||
|
private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
|
||||||
// SY -->
|
// 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,
|
private val controllerMode: SourceController.Mode,
|
||||||
// SY <--
|
// SY <--
|
||||||
) : BasePresenter<SourceController>() {
|
) : BasePresenter<SourceController>() {
|
||||||
|
|
||||||
var sources = getEnabledSources()
|
private val _state: MutableStateFlow<SourceState> = MutableStateFlow(SourceState.EMPTY)
|
||||||
|
val state: StateFlow<SourceState> = _state.asStateFlow()
|
||||||
/**
|
|
||||||
* Unsubscribe and create a new subscription to fetch enabled sources.
|
|
||||||
*/
|
|
||||||
private fun loadSources() {
|
|
||||||
val pinnedSources = mutableListOf<SourceItem>()
|
|
||||||
val pinnedSourceIds = preferences.pinnedSources().get()
|
|
||||||
|
|
||||||
|
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 -->
|
// SY -->
|
||||||
val categories = preferences.sourcesTabCategories().get()
|
presenterScope.launchIO {
|
||||||
.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it }))
|
getSourceCategories.subscribe()
|
||||||
.map {
|
.catch { exception ->
|
||||||
SourceCategory(it)
|
_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()
|
presenterScope.launchIO {
|
||||||
val sourcesAndCategories = if (sourcesAndCategoriesCombined.isNotEmpty()) sourcesAndCategoriesCombined.map {
|
getShowLatest.subscribe(mode = controllerMode)
|
||||||
val temp = it.split("|")
|
.catch { exception ->
|
||||||
temp[0] to temp[1]
|
_state.update { state ->
|
||||||
} else null
|
state.copy(sources = listOf(), error = exception)
|
||||||
|
}
|
||||||
val sourcesInCategories = sourcesAndCategories?.map { it.first }
|
}
|
||||||
|
.collectLatest(::updateShowLatest)
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
}
|
||||||
|
|
||||||
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
|
private suspend fun collectLatestSources(sources: List<Source>) {
|
||||||
|
val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
|
||||||
// Catalogues without a lang defined will be placed at the end
|
// Catalogues without a lang defined will be placed at the end
|
||||||
when {
|
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
|
d1 == "" && d2 != "" -> 1
|
||||||
d2 == "" && d1 != "" -> -1
|
d2 == "" && d1 != "" -> -1
|
||||||
else -> d1.compareTo(d2)
|
else -> d1.compareTo(d2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val byLang = sources.groupByTo(map) { it.lang }
|
val byLang = sources.groupByTo(map) {
|
||||||
var sourceItems = byLang.flatMap {
|
when {
|
||||||
val langItem = LangItem(it.key)
|
|
||||||
it.value.map { source ->
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val showPins = controllerMode == SourceController.Mode.CATALOGUE
|
it.category != null -> it.category
|
||||||
val showLatest = showPins && !preferences.useNewSourceNavigation().get()
|
|
||||||
// SY <--
|
// SY <--
|
||||||
val isPinned = source.id.toString() in pinnedSourceIds
|
it.isUsedLast -> LAST_USED_KEY
|
||||||
if (isPinned) {
|
Pin.Actual in it.pin -> PINNED_KEY
|
||||||
pinnedSources.add(SourceItem(source, LangItem(PINNED_KEY), isPinned /* SY --> */, showLatest, showPins /* SY <-- */))
|
else -> it.lang
|
||||||
}
|
|
||||||
|
|
||||||
// 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 <-- */)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_state.update { state ->
|
||||||
if (preferences.sourcesTabCategoriesFilter().get()) {
|
state.copy(
|
||||||
sourcesInCategories?.let { sourcesIds -> sourceItems = sourceItems.filterNot { it.source.id.toString() in sourcesIds } }
|
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() {
|
// SY -->
|
||||||
// Immediate initial load
|
private suspend fun updateCategories(categories: Set<String>) {
|
||||||
preferences.lastUsedSource().get().let { updateLastUsedSource(it) }
|
_state.update { state ->
|
||||||
|
state.copy(
|
||||||
// Subsequent updates
|
sourceCategories = categories.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it })
|
||||||
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 <-- */)
|
|
||||||
}
|
}
|
||||||
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() {
|
fun togglePin(source: Source) {
|
||||||
sources = getEnabledSources()
|
toggleSourcePin.await(source)
|
||||||
loadSources()
|
|
||||||
loadLastUsedSource()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun toggleExcludeFromDataSaver(source: Source) {
|
||||||
* Returns a list of enabled sources ordered by language and name.
|
toggleExcludeFromDataSaver.await(source)
|
||||||
*
|
}
|
||||||
* @return list containing enabled sources.
|
|
||||||
*/
|
|
||||||
private fun getEnabledSources(): List<CatalogueSource> {
|
|
||||||
val languages = preferences.enabledLanguages().get()
|
|
||||||
val disabledSourceIds = preferences.disabledSources().get()
|
|
||||||
|
|
||||||
return sourceManager.getVisibleCatalogueSources()
|
fun setSourceCategories(source: Source, categories: List<String>) {
|
||||||
.filter { it.lang in languages || it.id == LocalSource.ID }
|
setSourceCategories.await(source, categories)
|
||||||
.filterNot { it.id.toString() in disabledSourceIds }
|
|
||||||
.sortedBy { "(${it.lang}) ${it.name.lowercase()}" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -168,6 +159,29 @@ class SourcePresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
sealed class UiModel {
|
||||||
data class SourceCategory(val category: String, var sources: MutableList<SourceItem> = mutableListOf())
|
data class Item(val source: Source) : UiModel()
|
||||||
// SY <--
|
data class Header(val language: String, val isCategory: Boolean) : UiModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SourceState(
|
||||||
|
val sources: List<UiModel>,
|
||||||
|
val error: Throwable?,
|
||||||
|
val sourceCategories: List<String>,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,6 +20,7 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.kanade.domain.source.model.Source
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -80,8 +81,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
ChangeMangaCategoriesDialog.Listener {
|
ChangeMangaCategoriesDialog.Listener {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
source: CatalogueSource,
|
sourceId: Long,
|
||||||
searchQuery: String? = null,
|
query: String? = null,
|
||||||
// SY -->
|
// SY -->
|
||||||
smartSearchConfig: SourceController.SmartSearchConfig? = null,
|
smartSearchConfig: SourceController.SmartSearchConfig? = null,
|
||||||
savedSearch: Long? = null,
|
savedSearch: Long? = null,
|
||||||
@ -89,10 +90,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
// SY <--
|
// SY <--
|
||||||
) : this(
|
) : this(
|
||||||
Bundle().apply {
|
Bundle().apply {
|
||||||
putLong(SOURCE_ID_KEY, source.id)
|
putLong(SOURCE_ID_KEY, sourceId)
|
||||||
|
if (query != null) {
|
||||||
if (searchQuery != null) {
|
putString(SEARCH_QUERY_KEY, query)
|
||||||
putString(SEARCH_QUERY_KEY, searchQuery)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// 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()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.source.latest
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import eu.kanade.domain.source.model.Source
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
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) {
|
class LatestUpdatesController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||||
|
|
||||||
|
constructor(source: Source) : this(
|
||||||
|
bundleOf(SOURCE_ID_KEY to source.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
// SY -->
|
||||||
constructor(source: CatalogueSource) : this(
|
constructor(source: CatalogueSource) : this(
|
||||||
bundleOf(SOURCE_ID_KEY to source.id),
|
bundleOf(SOURCE_ID_KEY to source.id),
|
||||||
)
|
)
|
||||||
|
// SY <--
|
||||||
|
|
||||||
override fun createPresenter(): BrowseSourcePresenter {
|
override fun createPresenter(): BrowseSourcePresenter {
|
||||||
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
|
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
|
||||||
|
@ -69,6 +69,17 @@ class SettingsBrowseController : SettingsController() {
|
|||||||
}
|
}
|
||||||
// SY <--
|
// 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 {
|
preferenceCategory {
|
||||||
titleRes = R.string.label_extensions
|
titleRes = R.string.label_extensions
|
||||||
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/black"
|
|
||||||
android:pathData="M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z" />
|
|
||||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/black"
|
|
||||||
android:pathData="M14,4v5c0,1.12 0.37,2.16 1,3H9c0.65,-0.86 1,-1.9 1,-3V4H14M17,2H7C6.45,2 6,2.45 6,3c0,0.55 0.45,1 1,1c0,0 0,0 0,0l1,0v5c0,1.66 -1.34,3 -3,3v2h5.97v7l1,1l1,-1v-7H19v-2c0,0 0,0 0,0c-1.66,0 -3,-1.34 -3,-3V4l1,0c0,0 0,0 0,0c0.55,0 1,-0.45 1,-1C18,2.45 17.55,2 17,2L17,2z" />
|
|
||||||
</vector>
|
|
@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:paddingTop="8dp"
|
|
||||||
android:paddingBottom="@dimen/action_toolbar_list_padding"
|
|
||||||
tools:listitem="@layout/section_header_item" />
|
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
|
||||||
android:id="@+id/fast_scroller"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
app:fastScrollerBubbleEnabled="false"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
@ -39,6 +39,7 @@
|
|||||||
android:id="@+id/subtitle"
|
android:id="@+id/subtitle"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
@ -49,11 +50,13 @@
|
|||||||
tools:text="English"
|
tools:text="English"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/source_latest"
|
android:id="@+id/source_latest"
|
||||||
style="?attr/borderlessButtonStyle"
|
style="?attr/borderlessButtonStyle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
android:maxHeight="48dp"
|
android:maxHeight="48dp"
|
||||||
android:minWidth="0dp"
|
android:minWidth="0dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
@ -62,22 +65,9 @@
|
|||||||
android:text="@string/latest"
|
android:text="@string/latest"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/pin"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/pin"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/action_pin"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_push_pin_outline_24dp"
|
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -421,6 +421,8 @@
|
|||||||
<!-- Browse section -->
|
<!-- Browse section -->
|
||||||
<string name="pref_enable_automatic_extension_updates">Check for extension updates</string>
|
<string name="pref_enable_automatic_extension_updates">Check for extension updates</string>
|
||||||
<string name="pref_search_pinned_sources_only">Only include pinned sources</string>
|
<string name="pref_search_pinned_sources_only">Only include pinned sources</string>
|
||||||
|
<string name="pref_move_on_top">Move pins on top</string>
|
||||||
|
<string name="pref_move_on_top_summary">Move up pins to top of the source list</string>
|
||||||
|
|
||||||
<!-- Backup section -->
|
<!-- Backup section -->
|
||||||
<string name="pref_create_backup">Create backup</string>
|
<string name="pref_create_backup">Create backup</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user