mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Add refresh button to tracker for new profile info (#3828)
* Add refresh button to tracker for new profile info As-is, allows a user to manually refresh their current user data from the tracker in order to update Mihon's knowledge of remote settings (e.g. nickname changes, AL/MB/Kitsu score types, etc.). * Catch errors to prevent app crashing Surfaces errors as toasts in the same way library title refreshes do. * spotless * [skip ci] Remove leftover debugging logs * Fix Kitsu user attribute access * Spotless * Lowercase Kitsu score type on refresh Maybe prevent needing another #3887 for this. * Center tracker vertically name if not logged in Looks much better than the name being stuck at the top with empty space below it. * Don't show Refresh button for Enhanced trackers * [skip ci] Add changelog entry
This commit is contained in:
@@ -16,6 +16,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Allow `id:` to search for slugs on Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792))
|
||||
- Add support for using the user's chosen rating system for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3818](https://github.com/mihonapp/mihon/pull/3818))
|
||||
- Add support for the Year, Month, and Day fields in ComicInfo.xml files for chapter dating ([@MajorTanya](https://github.com/MajorTanya)) ([#3967](https://github.com/mihonapp/mihon/pull/3967))
|
||||
- Add refresh buttons to trackers in Settings to update displayed usernames (all trackers) & rating systems (where supported) ([@MajorTanya](https://github.com/MajorTanya)) ([#3828](https://github.com/mihonapp/mihon/pull/3828))
|
||||
|
||||
### Improved
|
||||
- Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
|
||||
|
||||
@@ -9,12 +9,14 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.InfoWidget
|
||||
@@ -26,7 +28,11 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TitleFontSize
|
||||
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
||||
import eu.kanade.tachiyomi.data.track.RefreshResult
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.BaseSliderItem
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
@@ -169,12 +175,28 @@ internal fun PreferenceItem(
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.TrackerPreference -> {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(item.tracker) {
|
||||
item.tracker.refreshResultFlow.collect {
|
||||
when (it) {
|
||||
RefreshResult.Success -> context.toast(
|
||||
context.stringResource(MR.strings.refresh_tracker_success, item.tracker.name),
|
||||
)
|
||||
|
||||
is RefreshResult.Error -> context.toast(
|
||||
context.stringResource(MR.strings.refresh_tracker_error, item.tracker.name, it.msg),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val isLoggedIn by item.tracker.let { tracker ->
|
||||
tracker.isLoggedInFlow.collectAsState(tracker.isLoggedIn)
|
||||
}
|
||||
val isRefreshing by item.tracker.isRefreshingFlow.collectAsState()
|
||||
TrackingPreferenceWidget(
|
||||
tracker = item.tracker,
|
||||
isLoggedIn = isLoggedIn,
|
||||
isRefreshing = isRefreshing,
|
||||
onClick = { if (isLoggedIn) item.logout() else item.login() },
|
||||
)
|
||||
}
|
||||
|
||||
+42
-1
@@ -7,10 +7,13 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -19,9 +22,13 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
|
||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.Done
|
||||
import mihon.icons.materialsymbols.rounded.Refresh
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@@ -30,16 +37,26 @@ fun TrackingPreferenceWidget(
|
||||
modifier: Modifier = Modifier,
|
||||
tracker: Tracker,
|
||||
isLoggedIn: Boolean,
|
||||
isRefreshing: Boolean,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val highlighted = LocalPreferenceHighlighted.current
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
Box(modifier = Modifier.highlightBackground(highlighted)) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
enabled = onClick != null,
|
||||
onClick = { onClick?.invoke() },
|
||||
)
|
||||
.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TrackLogoIcon(tracker)
|
||||
Column(
|
||||
@@ -64,7 +81,31 @@ fun TrackingPreferenceWidget(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isLoggedIn) {
|
||||
if (tracker !is EnhancedTracker) {
|
||||
IconButton(
|
||||
enabled = !isRefreshing,
|
||||
onClick = {
|
||||
scope.launch { tracker.refreshUser() }
|
||||
},
|
||||
) {
|
||||
if (!isRefreshing) {
|
||||
Icon(
|
||||
imageVector = MaterialSymbols.Rounded.Refresh,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.size(32.dp),
|
||||
contentDescription = stringResource(MR.strings.refresh_tracker_profile),
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = MaterialSymbols.Rounded.Done,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -8,7 +8,12 @@ import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import logcat.LogPriority
|
||||
import mihon.app.di.appGraph
|
||||
@@ -70,6 +75,29 @@ abstract class BaseTracker(
|
||||
}
|
||||
}
|
||||
|
||||
final override val isRefreshingFlow: StateFlow<Boolean>
|
||||
field: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
final override val refreshResultFlow: SharedFlow<RefreshResult>
|
||||
field: MutableSharedFlow<RefreshResult> = MutableSharedFlow(extraBufferCapacity = 1)
|
||||
|
||||
final override suspend fun refreshUser() {
|
||||
isRefreshingFlow.value = true
|
||||
try {
|
||||
updateUserConfig()
|
||||
refreshResultFlow.emit(RefreshResult.Success)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
logcat(LogPriority.ERROR, e) { "Failed to update user config id=$id" }
|
||||
refreshResultFlow.emit(RefreshResult.Error(e.message ?: "Failed with unknown error"))
|
||||
} finally {
|
||||
isRefreshingFlow.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// does the actual remote calls shielded from outside access to guarantee proper refresh flow setting
|
||||
protected abstract suspend fun updateUserConfig()
|
||||
|
||||
override fun getUsername() = trackPreferences.trackUsername(this).get()
|
||||
|
||||
override fun getDisplayUsername(): String = trackPreferences.trackDisplayUsername(this).get()
|
||||
@@ -151,3 +179,8 @@ abstract class BaseTracker(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface RefreshResult {
|
||||
data object Success : RefreshResult
|
||||
data class Error(val msg: String) : RefreshResult
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||
|
||||
@@ -57,10 +59,16 @@ interface Tracker {
|
||||
@CallSuper
|
||||
fun logout()
|
||||
|
||||
suspend fun refreshUser()
|
||||
|
||||
val isLoggedIn: Boolean
|
||||
|
||||
val isLoggedInFlow: Flow<Boolean>
|
||||
|
||||
val isRefreshingFlow: StateFlow<Boolean>
|
||||
|
||||
val refreshResultFlow: SharedFlow<RefreshResult>
|
||||
|
||||
fun getUsername(): String
|
||||
|
||||
fun getPassword(): String
|
||||
|
||||
@@ -214,6 +214,12 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() {
|
||||
val currentUser = api.getCurrentUser()
|
||||
scorePreference.set(currentUser.mediaListOptions.scoreFormat)
|
||||
saveDisplayUsername(currentUser.name)
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String) = login(password)
|
||||
|
||||
suspend fun login(token: String) {
|
||||
|
||||
@@ -120,6 +120,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() {
|
||||
val currentUser = api.getCurrentUser()
|
||||
saveDisplayUsername(currentUser.nickname?.takeIf { it.isNotBlank() } ?: currentUser.username)
|
||||
}
|
||||
|
||||
fun saveToken(oauth: BGMOAuth?) {
|
||||
trackPreferences.trackToken(this).set(json.encodeToString(oauth))
|
||||
}
|
||||
|
||||
@@ -146,6 +146,11 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker {
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() {
|
||||
val currentUser = api.getCurrentUser()
|
||||
saveDisplayUsername(currentUser.username)
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String) = login(password)
|
||||
|
||||
suspend fun login(reference: String) {
|
||||
|
||||
@@ -140,4 +140,6 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
||||
}
|
||||
authentications = oauth
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() = Unit
|
||||
}
|
||||
|
||||
@@ -196,6 +196,19 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
||||
saveCredentials(username, currentUser.id)
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() {
|
||||
val currentUser = api.getCurrentUser()
|
||||
|
||||
val ratingSystem = currentUser.ratingSystem
|
||||
if (ratingSystem.lowercase() in listOf(RATING_SIMPLE, RATING_REGULAR, RATING_ADVANCED)) {
|
||||
scorePreference.set(ratingSystem.lowercase())
|
||||
} else {
|
||||
logcat(LogPriority.ERROR) { "Unsupported Kitsu score type: $ratingSystem" }
|
||||
scorePreference.set(RATING_ADVANCED)
|
||||
}
|
||||
saveDisplayUsername(currentUser.profile.name)
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
interceptor.newAuth(null)
|
||||
|
||||
@@ -107,4 +107,6 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() = Unit
|
||||
}
|
||||
|
||||
@@ -143,6 +143,20 @@ class MangaBaka(id: Long) : BaseTracker(id, "MangaBaka"), DeletableTracker {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() {
|
||||
val currentUser = api.getCurrentUser()
|
||||
val scoreType = when (currentUser.ratingSteps) {
|
||||
1 -> STEP_1
|
||||
5 -> STEP_5
|
||||
10 -> STEP_10
|
||||
20 -> STEP_20
|
||||
25 -> STEP_25
|
||||
else -> throw Exception("Unknown score step size ${currentUser.ratingSteps}")
|
||||
}
|
||||
scorePreference.set(scoreType)
|
||||
saveDisplayUsername(currentUser.nickname ?: currentUser.preferredUsername ?: currentUser.id)
|
||||
}
|
||||
|
||||
fun saveToken(oauth: MangaBakaOAuth?) {
|
||||
trackPreferences.trackToken(this).set(json.encodeToString(oauth))
|
||||
}
|
||||
|
||||
@@ -124,6 +124,11 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
||||
saveCredentials(authenticated.uid.toString(), authenticated.sessionToken)
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() {
|
||||
val currentUser = api.getCurrentUser()
|
||||
saveDisplayUsername(currentUser.username)
|
||||
}
|
||||
|
||||
fun restoreSession(): String? {
|
||||
return trackPreferences.trackPassword(this).get().ifBlank { null }
|
||||
}
|
||||
|
||||
@@ -144,6 +144,11 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() {
|
||||
val username = api.getCurrentUser()
|
||||
saveDisplayUsername(username)
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
trackPreferences.trackToken(this).delete()
|
||||
|
||||
@@ -136,6 +136,11 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() {
|
||||
val currentUser = api.getCurrentUser()
|
||||
saveDisplayUsername(currentUser.nickname)
|
||||
}
|
||||
|
||||
fun saveToken(oauth: SMOAuth?) {
|
||||
trackPreferences.trackToken(this).set(json.encodeToString(oauth))
|
||||
}
|
||||
|
||||
@@ -108,4 +108,6 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
||||
val preferences = api.sourcePreferences()
|
||||
return preferences.getBoolean(TRACKER_DELETE_KEY, TRACKER_DELETE_DEFAULT)
|
||||
}
|
||||
|
||||
override suspend fun updateUserConfig() = Unit
|
||||
}
|
||||
|
||||
@@ -2,9 +2,15 @@ package eu.kanade.test
|
||||
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.RefreshResult
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import okhttp3.OkHttpClient
|
||||
import tachiyomi.domain.track.model.Track
|
||||
@@ -17,6 +23,8 @@ data class DummyTracker(
|
||||
override val supportsPrivateTracking: Boolean = false,
|
||||
override val isLoggedIn: Boolean = false,
|
||||
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
|
||||
override val isRefreshingFlow: StateFlow<Boolean> = MutableStateFlow(false),
|
||||
override val refreshResultFlow: SharedFlow<RefreshResult> = MutableSharedFlow<RefreshResult>().asSharedFlow(),
|
||||
val valLogo: Int = R.drawable.brand_anilist,
|
||||
val valStatuses: List<Long> = (1L..6L).toList(),
|
||||
val valReadingStatus: Long = 1L,
|
||||
@@ -75,6 +83,8 @@ data class DummyTracker(
|
||||
track: eu.kanade.tachiyomi.data.database.models.Track,
|
||||
): eu.kanade.tachiyomi.data.database.models.Track = track
|
||||
|
||||
override suspend fun refreshUser() = Unit
|
||||
|
||||
override suspend fun login(username: String, password: String) = Unit
|
||||
|
||||
override fun logout() = Unit
|
||||
|
||||
@@ -580,6 +580,9 @@
|
||||
<string name="enhanced_services_not_installed">Available but source not installed: %s</string>
|
||||
<string name="enhanced_tracking_info">Provides enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
|
||||
<string name="track_activity_name">Tracker login</string>
|
||||
<string name="refresh_tracker_profile">Refresh tracker profile</string>
|
||||
<string name="refresh_tracker_success">%s profile refreshed</string>
|
||||
<string name="refresh_tracker_error">%1$s refresh error: %2$s</string>
|
||||
|
||||
<!-- Browse section -->
|
||||
<string name="pref_hide_in_library_items">Hide entries already in library</string>
|
||||
|
||||
Reference in New Issue
Block a user