Show usernames for each tracker in settings (#3533)

This commit is contained in:
MajorTanya
2026-07-07 12:31:22 +02:00
committed by GitHub
parent 17aed02e56
commit d0c79399b9
24 changed files with 102 additions and 33 deletions
+3
View File
@@ -18,6 +18,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Display authors and description in Shikimori search results ([@MajorTanya](https://github.com/MajorTanya)) ([#3499](https://github.com/mihonapp/mihon/pull/3499)) - Display authors and description in Shikimori search results ([@MajorTanya](https://github.com/MajorTanya)) ([#3499](https://github.com/mihonapp/mihon/pull/3499))
- Invalidate download cache after backup restore ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3096](https://github.com/mihonapp/mihon/pull/3096)) - Invalidate download cache after backup restore ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3096](https://github.com/mihonapp/mihon/pull/3096))
### Improved
- Show usernames in Tracking settings ([@MajorTanya](https://github.com/MajorTanya)) ([#3533](https://github.com/mihonapp/mihon/pull/3533))
### Fixed ### Fixed
- Fix Shikimori tracking not working ([@MajorTanya](https://github.com/MajorTanya)) ([#3497](https://github.com/mihonapp/mihon/pull/3497)) - Fix Shikimori tracking not working ([@MajorTanya](https://github.com/MajorTanya)) ([#3497](https://github.com/mihonapp/mihon/pull/3497))
- Fix crash trying to select text in notes screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3516](https://github.com/mihonapp/mihon/pull/3516)) - Fix crash trying to select text in notes screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3516](https://github.com/mihonapp/mihon/pull/3516))
@@ -16,6 +16,11 @@ class TrackPreferences(
"", "",
) )
fun trackDisplayUsername(tracker: Tracker) = preferenceStore.getString(
Preference.privateKey("pref_mangasync_displayname_${tracker.id}"),
"",
)
fun trackPassword(tracker: Tracker) = preferenceStore.getString( fun trackPassword(tracker: Tracker) = preferenceStore.getString(
Preference.privateKey("pref_mangasync_password_${tracker.id}"), Preference.privateKey("pref_mangasync_password_${tracker.id}"),
"", "",
@@ -171,7 +171,7 @@ internal fun PreferenceItem(
} }
TrackingPreferenceWidget( TrackingPreferenceWidget(
tracker = item.tracker, tracker = item.tracker,
checked = isLoggedIn, isLoggedIn = isLoggedIn,
onClick = { if (isLoggedIn) item.logout() else item.login() }, onClick = { if (isLoggedIn) item.logout() else item.login() },
) )
} }
@@ -2,6 +2,7 @@ package eu.kanade.presentation.more.settings.widget
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -15,6 +16,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
import eu.kanade.presentation.track.components.TrackLogoIcon import eu.kanade.presentation.track.components.TrackLogoIcon
@@ -26,7 +29,7 @@ import tachiyomi.presentation.core.i18n.stringResource
fun TrackingPreferenceWidget( fun TrackingPreferenceWidget(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tracker: Tracker, tracker: Tracker,
checked: Boolean, isLoggedIn: Boolean,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
val highlighted = LocalPreferenceHighlighted.current val highlighted = LocalPreferenceHighlighted.current
@@ -39,16 +42,29 @@ fun TrackingPreferenceWidget(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
TrackLogoIcon(tracker) TrackLogoIcon(tracker)
Text( Column(
text = tracker.name, modifier = Modifier.weight(1f),
modifier = Modifier ) {
.weight(1f) Text(
.padding(horizontal = 16.dp), text = tracker.name,
maxLines = 1, modifier = Modifier.padding(horizontal = 16.dp),
style = MaterialTheme.typography.titleLarge, maxLines = 1,
fontSize = TitleFontSize, style = MaterialTheme.typography.titleLarge,
) fontSize = TitleFontSize,
if (checked) { fontWeight = FontWeight.Medium,
)
val displayName = tracker.getDisplayUsername()
if (isLoggedIn && displayName.isNotBlank()) {
Text(
text = displayName,
modifier = Modifier.padding(horizontal = 16.dp),
maxLines = 1,
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis,
)
}
}
if (isLoggedIn) {
Icon( Icon(
imageVector = Icons.Outlined.Done, imageVector = Icons.Outlined.Done,
modifier = Modifier modifier = Modifier
@@ -68,6 +68,10 @@ abstract class BaseTracker(
override fun getUsername() = trackPreferences.trackUsername(this).get() override fun getUsername() = trackPreferences.trackUsername(this).get()
override fun getDisplayUsername(): String = trackPreferences.trackDisplayUsername(this).get()
override fun saveDisplayUsername(displayName: String) = trackPreferences.trackDisplayUsername(this).set(displayName)
override fun getPassword() = trackPreferences.trackPassword(this).get() override fun getPassword() = trackPreferences.trackPassword(this).get()
override fun saveCredentials(username: String, password: String) { override fun saveCredentials(username: String, password: String) {
@@ -65,6 +65,10 @@ interface Tracker {
fun getPassword(): String fun getPassword(): String
fun getDisplayUsername(): String
fun saveDisplayUsername(displayName: String)
fun saveCredentials(username: String, password: String) fun saveCredentials(username: String, password: String)
// TODO: move this to an interactor, and update all trackers based on common data // TODO: move this to an interactor, and update all trackers based on common data
@@ -213,9 +213,10 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
try { try {
val oauth = api.createOAuth(token) val oauth = api.createOAuth(token)
interceptor.setAuth(oauth) interceptor.setAuth(oauth)
val (username, scoreType) = api.getCurrentUser() val currentUser = api.getCurrentUser()
scorePreference.set(scoreType) scorePreference.set(currentUser.mediaListOptions.scoreFormat)
saveCredentials(username.toString(), oauth.accessToken) saveDisplayUsername(currentUser.name)
saveCredentials(currentUser.id.toString(), oauth.accessToken)
} catch (e: Throwable) { } catch (e: Throwable) {
logout() logout()
} }
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserViewerData
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
@@ -285,12 +286,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000) return ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
} }
suspend fun getCurrentUser(): Pair<Int, String> { suspend fun getCurrentUser(): ALUserViewerData {
return withIOContext { return withIOContext {
val query = """ val query = """
|query User { |query User {
|Viewer { |Viewer {
|id |id
|name
|mediaListOptions { |mediaListOptions {
|scoreFormat |scoreFormat
|} |}
@@ -310,10 +312,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
) )
.awaitSuccess() .awaitSuccess()
.parseAs<ALCurrentUserResult>() .parseAs<ALCurrentUserResult>()
.let { .data.viewer
val viewer = it.data.viewer
Pair(viewer.id, viewer.mediaListOptions.scoreFormat)
}
} }
} }
} }
@@ -17,6 +17,7 @@ data class ALUserViewer(
@Serializable @Serializable
data class ALUserViewerData( data class ALUserViewerData(
val id: Int, val id: Int,
val name: String,
val mediaListOptions: ALUserListOptions, val mediaListOptions: ALUserListOptions,
) )
@@ -106,8 +106,9 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
// Users can set a 'username' (not nickname) once which effectively // Users can set a 'username' (not nickname) once which effectively
// replaces the stringified ID in certain queries. // replaces the stringified ID in certain queries.
// If no username is set, the API returns the user ID as a strings // If no username is set, the API returns the user ID as a strings
val username = api.getUsername() val currentUser = api.getCurrentUser()
saveCredentials(username, oauth.accessToken) saveDisplayUsername(currentUser.nickname?.takeIf { it.isNotBlank() } ?: currentUser.username)
saveCredentials(currentUser.username, oauth.accessToken)
} catch (_: Throwable) { } catch (_: Throwable) {
logout() logout()
} }
@@ -153,13 +153,12 @@ class BangumiApi(
} }
} }
suspend fun getUsername(): String { suspend fun getCurrentUser(): BGMUser {
return withIOContext { return withIOContext {
with(json) { with(json) {
authClient.newCall(GET("$API_URL/v0/me")) authClient.newCall(GET("$API_URL/v0/me"))
.awaitSuccess() .awaitSuccess()
.parseAs<BGMUser>() .parseAs<BGMUser>()
.username
} }
} }
} }
@@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
// Incomplete DTO with only our needed attributes // Incomplete DTO with only our needed attributes
data class BGMUser( data class BGMUser(
val username: String, val username: String,
val nickname: String?,
) )
@@ -144,6 +144,7 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker {
val oauth = api.accessToken(reference) val oauth = api.accessToken(reference)
interceptor.setAuth(oauth) interceptor.setAuth(oauth)
val user = api.getCurrentUser() val user = api.getCurrentUser()
saveDisplayUsername(user.username)
saveCredentials(user.reference, oauth.accessToken) saveCredentials(user.reference, oauth.accessToken)
} catch (_: Throwable) { } catch (_: Throwable) {
logout() logout()
@@ -127,8 +127,9 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
override suspend fun login(username: String, password: String) { override suspend fun login(username: String, password: String) {
val token = api.login(username, password) val token = api.login(username, password)
interceptor.newAuth(token) interceptor.newAuth(token)
val userId = api.getCurrentUser() val currentUser = api.getCurrentUser()
saveCredentials(username, userId) saveDisplayUsername(currentUser.attributes.name)
saveCredentials(username, currentUser.id)
} }
override fun logout() { override fun logout() {
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuUser
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@@ -227,7 +228,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
suspend fun getCurrentUser(): String { suspend fun getCurrentUser(): KitsuUser {
return withIOContext { return withIOContext {
val url = "${BASE_URL}users".toUri().buildUpon() val url = "${BASE_URL}users".toUri().buildUpon()
.encodedQuery("filter[self]=true") .encodedQuery("filter[self]=true")
@@ -237,7 +238,6 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.awaitSuccess() .awaitSuccess()
.parseAs<KitsuCurrentUserResult>() .parseAs<KitsuCurrentUserResult>()
.data[0] .data[0]
.id
} }
} }
} }
@@ -10,4 +10,10 @@ data class KitsuCurrentUserResult(
@Serializable @Serializable
data class KitsuUser( data class KitsuUser(
val id: String, val id: String,
val attributes: KitsuUserAttributes,
)
@Serializable
data class KitsuUserAttributes(
val name: String,
) )
@@ -107,8 +107,10 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
override suspend fun login(username: String, password: String) { override suspend fun login(username: String, password: String) {
val authenticated = api.authenticate(username, password) val authenticated = api.authenticate(username, password)
saveCredentials(authenticated.uid.toString(), authenticated.sessionToken)
interceptor.newAuth(authenticated.sessionToken) interceptor.newAuth(authenticated.sessionToken)
val currentUser = api.getCurrentUser()
saveDisplayUsername(currentUser.username)
saveCredentials(authenticated.uid.toString(), authenticated.sessionToken)
} }
fun restoreSession(): String? { fun restoreSession(): String? {
@@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUContext import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUContext
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUCurrentUser
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MUListItem
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MULoginResponse import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MULoginResponse
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating import eu.kanade.tachiyomi.data.track.mangaupdates.dto.MURating
@@ -190,6 +191,14 @@ class MangaUpdatesApi(
} }
} }
suspend fun getCurrentUser(): MUCurrentUser {
return with(json) {
authClient.newCall(GET("$BASE_URL/v1/account/profile"))
.awaitSuccess()
.parseAs<MUCurrentUser>()
}
}
companion object { companion object {
private const val BASE_URL = "https://api.mangaupdates.com" private const val BASE_URL = "https://api.mangaupdates.com"
@@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
import kotlinx.serialization.Serializable
@Serializable
data class MUCurrentUser(
val username: String,
)
@@ -137,6 +137,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
val oauth = api.getAccessToken(authCode) val oauth = api.getAccessToken(authCode)
interceptor.setAuth(oauth) interceptor.setAuth(oauth)
val username = api.getCurrentUser() val username = api.getCurrentUser()
saveDisplayUsername(username)
saveCredentials(username, oauth.accessToken) saveCredentials(username, oauth.accessToken)
} catch (e: Throwable) { } catch (e: Throwable) {
logout() logout()
@@ -122,7 +122,8 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
val oauth = api.accessToken(code) val oauth = api.accessToken(code)
interceptor.newAuth(oauth) interceptor.newAuth(oauth)
val user = api.getCurrentUser() val user = api.getCurrentUser()
saveCredentials(user.toString(), oauth.accessToken) saveDisplayUsername(user.nickname)
saveCredentials(user.id, oauth.accessToken)
} catch (e: Throwable) { } catch (e: Throwable) {
logout() logout()
} }
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMAddMangaResponse import eu.kanade.tachiyomi.data.track.shikimori.dto.SMAddMangaResponse
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMSearchResult import eu.kanade.tachiyomi.data.track.shikimori.dto.SMSearchResult
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListResult import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListResult
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserResult import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserResult
import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.DELETE
@@ -171,12 +172,13 @@ class ShikimoriApi(
} }
} }
suspend fun getCurrentUser(): Int { suspend fun getCurrentUser(): SMUser {
return with(json) { return with(json) {
val query = """ val query = """
|{ |{
|currentUser { |currentUser {
|id |id
|nickname
|} |}
|} |}
""".trimMargin() """.trimMargin()
@@ -191,8 +193,7 @@ class ShikimoriApi(
) )
.awaitSuccess() .awaitSuccess()
.parseAs<SMUserResult>() .parseAs<SMUserResult>()
.data.currentUser.id .data.currentUser
.toInt()
} }
} }
@@ -15,4 +15,5 @@ data class SMCurrentUser(
@Serializable @Serializable
data class SMUser( data class SMUser(
val id: String, val id: String,
val nickname: String,
) )
@@ -81,6 +81,10 @@ data class DummyTracker(
override fun getUsername(): String = "username" override fun getUsername(): String = "username"
override fun getDisplayUsername(): String = "UserName"
override fun saveDisplayUsername(displayName: String): Unit = Unit
override fun getPassword(): String = "passw0rd" override fun getPassword(): String = "passw0rd"
override fun saveCredentials(username: String, password: String) = Unit override fun saveCredentials(username: String, password: String) = Unit