Observe tracker login state instead of fetching once (#987)
Some checks failed
CI / Build app (push) Has been cancelled

* Observe tracker login state instead of fetching once

* Review changes
This commit is contained in:
AntsyLich 2024-07-06 07:25:33 +06:00 committed by GitHub
parent 5a61ca5535
commit 2092c81bad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 96 additions and 53 deletions

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -125,7 +126,7 @@ private fun ColumnScope.FilterPage(
) )
} }
val trackers = remember { screenModel.trackers } val trackers by screenModel.trackersFlow.collectAsState()
when (trackers.size) { when (trackers.size) {
0 -> { 0 -> {
// No trackers // No trackers
@ -158,11 +159,11 @@ private fun ColumnScope.SortPage(
category: Category?, category: Category?,
screenModel: LibrarySettingsScreenModel, screenModel: LibrarySettingsScreenModel,
) { ) {
val trackers by screenModel.trackersFlow.collectAsState()
val sortingMode = category.sort.type val sortingMode = category.sort.type
val sortDescending = !category.sort.isAscending val sortDescending = !category.sort.isAscending
val trackerSortOption = val trackerSortOption = if (trackers.isEmpty()) {
if (screenModel.trackers.isEmpty()) {
emptyList() emptyList()
} else { } else {
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean) listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean)

View file

@ -7,12 +7,12 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.InfoWidget import eu.kanade.presentation.more.settings.widget.InfoWidget
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
@ -23,8 +23,6 @@ import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp } val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp }
@ -156,17 +154,15 @@ internal fun PreferenceItem(
) )
} }
is Preference.PreferenceItem.TrackerPreference -> { is Preference.PreferenceItem.TrackerPreference -> {
val uName by Injekt.get<TrackPreferences>() val isLoggedIn by item.tracker.let { tracker ->
.trackUsername(item.tracker) tracker.isLoggedInFlow.collectAsState(tracker.isLoggedIn)
.collectAsState() }
item.tracker.run {
TrackingPreferenceWidget( TrackingPreferenceWidget(
tracker = this, tracker = item.tracker,
checked = uName.isNotEmpty(), checked = isLoggedIn,
onClick = { if (isLoggedIn) item.logout() else item.login() }, onClick = { if (isLoggedIn) item.logout() else item.login() },
) )
} }
}
is Preference.PreferenceItem.InfoPreference -> { is Preference.PreferenceItem.InfoPreference -> {
InfoWidget(text = item.title) InfoWidget(text = item.title)
} }

View file

@ -8,6 +8,8 @@ import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import logcat.LogPriority import logcat.LogPriority
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@ -53,6 +55,15 @@ abstract class BaseTracker(
get() = getUsername().isNotEmpty() && get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty() getPassword().isNotEmpty()
override val isLoggedInFlow: Flow<Boolean> by lazy {
combine(
trackPreferences.trackUsername(this).changes(),
trackPreferences.trackPassword(this).changes(),
) { username, password ->
username.isNotEmpty() && password.isNotEmpty()
}
}
override fun getUsername() = trackPreferences.trackUsername(this).get() override fun getUsername() = trackPreferences.trackUsername(this).get()
override fun getPassword() = trackPreferences.trackPassword(this).get() override fun getPassword() = trackPreferences.trackPassword(this).get()

View file

@ -7,6 +7,7 @@ import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
@ -61,6 +62,8 @@ interface Tracker {
val isLoggedIn: Boolean val isLoggedIn: Boolean
val isLoggedInFlow: Flow<Boolean>
fun getUsername(): String fun getUsername(): String
fun getPassword(): String fun getPassword(): String

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
import kotlinx.coroutines.flow.combine
class TrackerManager { class TrackerManager {
@ -32,5 +33,13 @@ class TrackerManager {
fun loggedInTrackers() = trackers.filter { it.isLoggedIn } fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
fun get(id: Long) = trackers.find { it.id == id } fun loggedInTrackersFlow() = combine(trackers.map { it.isLoggedInFlow }) {
it.mapIndexedNotNull { index, isLoggedIn ->
if (isLoggedIn) trackers[index] else null
}
}
fun get(id: Long) = trackers.find { it.id == id }
fun getAll(ids: Set<Long>) = trackers.filter { it.id in ids }
} }

View file

@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -106,10 +107,10 @@ class LibraryScreenModel(
getTracksPerManga.subscribe(), getTracksPerManga.subscribe(),
getTrackingFilterFlow(), getTrackingFilterFlow(),
downloadCache.changes, downloadCache.changes,
) { searchQuery, library, tracks, loggedInTrackers, _ -> ) { searchQuery, library, tracks, trackingFiler, _ ->
library library
.applyFilters(tracks, loggedInTrackers) .applyFilters(tracks, trackingFiler)
.applySort(tracks) .applySort(tracks, trackingFiler.keys)
.mapValues { (_, value) -> .mapValues { (_, value) ->
if (searchQuery != null) { if (searchQuery != null) {
// Filter query // Filter query
@ -173,9 +174,10 @@ class LibraryScreenModel(
/** /**
* Applies library filters to the given map of manga. * Applies library filters to the given map of manga.
*/ */
@Suppress("LongMethod", "CyclomaticComplexMethod")
private suspend fun LibraryMap.applyFilters( private suspend fun LibraryMap.applyFilters(
trackMap: Map<Long, List<Track>>, trackMap: Map<Long, List<Track>>,
loggedInTrackers: Map<Long, TriState>, trackingFiler: Map<Long, TriState>,
): LibraryMap { ): LibraryMap {
val prefs = getLibraryItemPreferencesFlow().first() val prefs = getLibraryItemPreferencesFlow().first()
val downloadedOnly = prefs.globalFilterDownloaded val downloadedOnly = prefs.globalFilterDownloaded
@ -187,10 +189,10 @@ class LibraryScreenModel(
val filterCompleted = prefs.filterCompleted val filterCompleted = prefs.filterCompleted
val filterIntervalCustom = prefs.filterIntervalCustom val filterIntervalCustom = prefs.filterIntervalCustom
val isNotLoggedInAnyTrack = loggedInTrackers.isEmpty() val isNotLoggedInAnyTrack = trackingFiler.isEmpty()
val excludedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } val excludedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
val includedTracks = loggedInTrackers.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } val includedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
val filterFnDownloaded: (LibraryItem) -> Boolean = { val filterFnDownloaded: (LibraryItem) -> Boolean = {
@ -254,9 +256,11 @@ class LibraryScreenModel(
/** /**
* Applies library sorting to the given map of manga. * Applies library sorting to the given map of manga.
*/ */
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun LibraryMap.applySort( private fun LibraryMap.applySort(
// Map<MangaId, List<Track>> // Map<MangaId, List<Track>>
trackMap: Map<Long, List<Track>>, trackMap: Map<Long, List<Track>>,
loggedInTrackerIds: Set<Long>
): LibraryMap { ): LibraryMap {
val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase()) i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase())
@ -264,7 +268,7 @@ class LibraryScreenModel(
val defaultTrackerScoreSortValue = -1.0 val defaultTrackerScoreSortValue = -1.0
val trackerScores by lazy { val trackerScores by lazy {
val trackerMap = trackerManager.loggedInTrackers().associateBy { e -> e.id } val trackerMap = trackerManager.getAll(loggedInTrackerIds).associateBy { e -> e.id }
trackMap.mapValues { entry -> trackMap.mapValues { entry ->
when { when {
entry.value.isEmpty() -> null entry.value.isEmpty() -> null
@ -405,18 +409,17 @@ class LibraryScreenModel(
* @return map of track id with the filter value * @return map of track id with the filter value
*/ */
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> { private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
val loggedInTrackers = trackerManager.loggedInTrackers() return trackerManager.loggedInTrackersFlow().flatMapLatest { loggedInTrackers ->
return if (loggedInTrackers.isNotEmpty()) { if (loggedInTrackers.isEmpty()) return@flatMapLatest flowOf(emptyMap())
val prefFlows = loggedInTrackers
.map { libraryPreferences.filterTracking(it.id.toInt()).changes() } val prefFlows = loggedInTrackers.map { tracker ->
.toTypedArray() libraryPreferences.filterTracking(tracker.id.toInt()).changes()
combine(*prefFlows) { }
combine(prefFlows) {
loggedInTrackers loggedInTrackers
.mapIndexed { index, tracker -> tracker.id to it[index] } .mapIndexed { index, tracker -> tracker.id to it[index] }
.toMap() .toMap()
} }
} else {
flowOf(emptyMap())
} }
} }

View file

@ -4,6 +4,8 @@ import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
@ -16,17 +18,22 @@ import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
class LibrarySettingsScreenModel( class LibrarySettingsScreenModel(
val preferences: BasePreferences = Injekt.get(), val preferences: BasePreferences = Injekt.get(),
val libraryPreferences: LibraryPreferences = Injekt.get(), val libraryPreferences: LibraryPreferences = Injekt.get(),
private val setDisplayMode: SetDisplayMode = Injekt.get(), private val setDisplayMode: SetDisplayMode = Injekt.get(),
private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(), private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(), trackerManager: TrackerManager = Injekt.get(),
) : ScreenModel { ) : ScreenModel {
val trackers val trackersFlow = trackerManager.loggedInTrackersFlow()
get() = trackerManager.trackers.filter { it.isLoggedIn } .stateIn(
scope = screenModelScope,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
initialValue = trackerManager.loggedInTrackers()
)
fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) { fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) {
preference(libraryPreferences).getAndSet { preference(libraryPreferences).getAndSet {

View file

@ -138,7 +138,7 @@ class MangaScreen(
) )
}.takeIf { isHttpSource }, }.takeIf { isHttpSource },
onTrackingClicked = { onTrackingClicked = {
if (screenModel.loggedInTrackers.isEmpty()) { if (successState.loggedInTracker.isEmpty()) {
navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking)) navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking))
} else { } else {
screenModel.showTrackDialog() screenModel.showTrackDialog()

View file

@ -28,6 +28,7 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -117,8 +117,6 @@ class MangaScreenModel(
private val successState: State.Success? private val successState: State.Success?
get() = state.value as? State.Success get() = state.value as? State.Success
val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } }
val manga: Manga? val manga: Manga?
get() = successState?.manga get() = successState?.manga
@ -194,6 +192,16 @@ class MangaScreenModel(
} }
} }
screenModelScope.launchIO {
trackerManager.loggedInTrackersFlow()
.distinctUntilChanged()
.collectLatest { trackers ->
updateSuccessState {
it.copy(loggedInTracker = trackers)
}
}
}
observeDownloads() observeDownloads()
screenModelScope.launchIO { screenModelScope.launchIO {
@ -978,12 +986,13 @@ class MangaScreenModel(
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
screenModelScope.launchIO { screenModelScope.launchIO {
getTracks.subscribe(manga.id) combine(
.catch { logcat(LogPriority.ERROR, it) } getTracks.subscribe(manga.id).catch { logcat(LogPriority.ERROR, it) },
.map { tracks -> trackerManager.loggedInTrackersFlow(),
) { mangaTracks, loggedInTrackers ->
loggedInTrackers loggedInTrackers
// Map to TrackItem // Map to TrackItem
.map { service -> TrackItem(tracks.find { it.trackerId == service.id }, service) } .map { service -> TrackItem(mangaTracks.find { it.trackerId == service.id }, service) }
// Show only if the service supports this manga's source // Show only if the service supports this manga's source
.filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true } .filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true }
} }
@ -1057,6 +1066,7 @@ class MangaScreenModel(
val isRefreshingData: Boolean = false, val isRefreshingData: Boolean = false,
val dialog: Dialog? = null, val dialog: Dialog? = null,
val hasPromptedToAddBefore: Boolean = false, val hasPromptedToAddBefore: Boolean = false,
val loggedInTracker: List<Tracker> = emptyList(),
) : State { ) : State {
val processedChapters by lazy { val processedChapters by lazy {
chapters.applyFilters(manga).toList() chapters.applyFilters(manga).toList()

View file

@ -239,7 +239,7 @@ data class TrackInfoDialogHomeScreen(
} }
private fun List<Track>.mapToTrackItem(): List<TrackItem> { private fun List<Track>.mapToTrackItem(): List<TrackItem> {
val loggedInTrackers = Injekt.get<TrackerManager>().trackers.filter { it.isLoggedIn } val loggedInTrackers = Injekt.get<TrackerManager>().loggedInTrackers()
val source = Injekt.get<SourceManager>().getOrStub(sourceId) val source = Injekt.get<SourceManager>().getOrStub(sourceId)
return loggedInTrackers return loggedInTrackers
// Map to TrackItem // Map to TrackItem

View file

@ -36,7 +36,7 @@ class StatsScreenModel(
private val trackerManager: TrackerManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(),
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) { ) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) {
private val loggedInTrackers by lazy { trackerManager.trackers.fastFilter { it.isLoggedIn } } private val loggedInTrackers by lazy { trackerManager.loggedInTrackers() }
init { init {
screenModelScope.launchIO { screenModelScope.launchIO {

View file

@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -16,6 +18,7 @@ data class DummyTracker(
override val name: String, override val name: String,
override val supportsReadingDates: Boolean = false, override val supportsReadingDates: Boolean = false,
override val isLoggedIn: Boolean = false, override val isLoggedIn: Boolean = false,
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
val valLogoColor: Int = Color.rgb(18, 25, 35), val valLogoColor: Int = Color.rgb(18, 25, 35),
val valLogo: Int = R.drawable.ic_tracker_anilist, val valLogo: Int = R.drawable.ic_tracker_anilist,
val valStatuses: List<Long> = (1L..6L).toList(), val valStatuses: List<Long> = (1L..6L).toList(),