ui: Real account button (avatar + display name), stable list keys
The top bar now mirrors SmartTube: a 44dp circle with the actual user avatar (persisted at login alongside the display name; falls back to the person icon) and the display name next to it. Scrolling jank: add stable keys to grids and rows (item/channel ids) - without keys Compose rebinds the whole list during scroll and re-fires every AsyncImage + focus request, which is the realistic culprit for the 2fps vertical scrolling on the TV.
This commit is contained in:
parent
cc1c1d1c9d
commit
f7273121ae
7 changed files with 61 additions and 16 deletions
|
|
@ -86,7 +86,14 @@ class PeerTubeRepository(
|
|||
.addConverterFactory(json.asConverterFactory("application/json; charset=utf-8".toMediaType()))
|
||||
.build()
|
||||
.create(PeerTubeApi::class.java)
|
||||
store.accountName = meApi.me().username
|
||||
val me = meApi.me()
|
||||
store.accountName = me.username
|
||||
store.userDisplayName = me.displayName ?: me.username
|
||||
me.avatars.maxByOrNull { it.width }?.let { avatar ->
|
||||
store.userAvatarUrl = avatar.fileUrl
|
||||
?.takeIf { url -> url.startsWith("http") }
|
||||
?: apiUrlOf(base) + avatar.path
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
store.accountName = username.substringBefore('@') // best effort fallback
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,5 +137,6 @@ data class VideoTokenFiles(
|
|||
@Serializable
|
||||
data class UserDto(
|
||||
val username: String,
|
||||
val displayName: String? = null,
|
||||
val avatars: List<Avatar> = emptyList(),
|
||||
)
|
||||
|
|
@ -48,6 +48,15 @@ class TokenStore(context: Context) {
|
|||
get() = prefs.getString("refreshToken", null)
|
||||
private set(value) = prefs.edit().putString("refreshToken", value).apply()
|
||||
|
||||
/** Display name + avatar for the account button (from users/me at login). */
|
||||
var userDisplayName: String?
|
||||
get() = prefs.getString("userDisplayName", null)
|
||||
set(value) = prefs.edit().putString("userDisplayName", value).apply()
|
||||
|
||||
var userAvatarUrl: String?
|
||||
get() = prefs.getString("userAvatarUrl", null)
|
||||
set(value) = prefs.edit().putString("userAvatarUrl", value).apply()
|
||||
|
||||
/** Real account name from users/me — may differ from the login input (email). */
|
||||
var accountName: String?
|
||||
get() = prefs.getString("accountName", null)
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ private fun ChannelCardRow(
|
|||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
itemsIndexed(channels) { index, channel ->
|
||||
itemsIndexed(channels, key = { _, c -> c.name + "@" + c.host }) { index, channel ->
|
||||
val fr = remember { FocusRequester() }
|
||||
focusRequesters[index] = fr
|
||||
ChannelCard(
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ fun FocusScrollRow(
|
|||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
itemsIndexed(videos) { index, video ->
|
||||
itemsIndexed(videos, key = { _, video -> "v" + video.id }) { index, video ->
|
||||
val fr = remember { FocusRequester() }
|
||||
focusRequesters[index] = fr
|
||||
VideoCard(
|
||||
|
|
|
|||
|
|
@ -239,31 +239,53 @@ private fun TopBar(
|
|||
desc = "Search",
|
||||
onClick = onSearchClick,
|
||||
)
|
||||
// "Who is logged in" — account button with the username (SmartTube shows the user).
|
||||
// "Who is logged in" — account button: avatar circle + display name (SmartTube).
|
||||
val accountFocused = remember { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.onFocusChanged { accountFocused.value = it.isFocused }
|
||||
.background(if (accountFocused.value) Color(0x33FFFFFF) else Color.Transparent)
|
||||
.background(Color.Transparent)
|
||||
.then(
|
||||
if (accountFocused.value) Modifier.border(3.dp, Color.White, CircleShape)
|
||||
else Modifier
|
||||
)
|
||||
.focusable()
|
||||
.clickable(onClick = onAccountClick)
|
||||
.padding(6.dp),
|
||||
.clickable(onClick = onAccountClick),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFFCCCCCC),
|
||||
)
|
||||
val avatarUrl = AppGraph.tokenStore.userAvatarUrl
|
||||
if (avatarUrl != null) {
|
||||
coil.compose.AsyncImage(
|
||||
model = coil.request.ImageRequest.Builder(androidx.compose.ui.platform.LocalContext.current)
|
||||
.data(avatarUrl)
|
||||
.size(96)
|
||||
.build(),
|
||||
contentDescription = "Account",
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF262626)),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF262626)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
contentDescription = "Account",
|
||||
tint = Color(0xFFCCCCCC),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = AppGraph.tokenStore.username?.substringBefore('@') ?: "Account",
|
||||
color = Color(0xFFCCCCCC),
|
||||
text = AppGraph.tokenStore.userDisplayName
|
||||
?: AppGraph.tokenStore.username?.substringBefore('@') ?: "Account",
|
||||
color = Color(0xFFB0B0B0),
|
||||
style = androidx.compose.material3.MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ fun VideoListScreen(
|
|||
state = gridState,
|
||||
modifier = Modifier.fillMaxSize().padding(vertical = 8.dp),
|
||||
) {
|
||||
itemsIndexed(items) { index, item ->
|
||||
itemsIndexed(items, key = { _, item -> itemStableKey(item) }) { index, item ->
|
||||
val video: VideoDto = when (item) {
|
||||
is VideoListItem.HistoryEntry -> item.history.video
|
||||
is VideoListItem.Video -> item.video
|
||||
|
|
@ -189,6 +189,12 @@ private fun SearchHeader(viewModel: VideoListViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun itemStableKey(item: VideoListItem): String =
|
||||
when (item) {
|
||||
is VideoListItem.Video -> "v" + item.video.id
|
||||
is VideoListItem.HistoryEntry -> "h" + item.history.video.id
|
||||
}
|
||||
|
||||
private fun sourceKey(source: ListSource): String = when (source) {
|
||||
is ListSource.Channel -> "channel:${source.handle}"
|
||||
else -> source::class.java.simpleName
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue