ui: Rebuild all screens when the session epoch changes
Signing out and logging in as another account kept showing the previous account's videos: the list ViewModels live in the activity store and are keyed by destination only. A session epoch now increments on logout and on each successful login; every ViewModel key, grid scroll state and per-item focus map is keyed by it, so an account switch starts from genuinely fresh data.
This commit is contained in:
parent
9d89c1c060
commit
14cd3d3976
6 changed files with 64 additions and 14 deletions
|
|
@ -19,6 +19,13 @@ class AppViewModel(private val repository: PeerTubeRepository = AppGraph.reposit
|
|||
private val _sessionState = MutableStateFlow<SessionState>(SessionState.Loading)
|
||||
val sessionState: StateFlow<SessionState> = _sessionState
|
||||
|
||||
/**
|
||||
* Bumped on every logout and successful login: all list ViewModels and saved
|
||||
* UI state are keyed by it, so switching accounts never shows the previous
|
||||
* account's cached content.
|
||||
*/
|
||||
val sessionEpoch = MutableStateFlow(0)
|
||||
|
||||
val loginError = MutableStateFlow<String?>(null)
|
||||
val loggingIn = MutableStateFlow(false)
|
||||
|
||||
|
|
@ -35,6 +42,7 @@ class AppViewModel(private val repository: PeerTubeRepository = AppGraph.reposit
|
|||
loggingIn.value = false
|
||||
if (result.isSuccess) {
|
||||
_sessionState.value = SessionState.Ready
|
||||
sessionEpoch.value += 1
|
||||
} else {
|
||||
loginError.value = if (result.exceptionOrNull() is PeerTubeRepository.BadCredentialsException) {
|
||||
"Invalid credentials"
|
||||
|
|
@ -60,5 +68,6 @@ class AppViewModel(private val repository: PeerTubeRepository = AppGraph.reposit
|
|||
fun logout() {
|
||||
repository.signOut()
|
||||
_sessionState.value = SessionState.NeedsSetup
|
||||
sessionEpoch.value += 1
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,8 @@ class ChannelsViewModel(private val store: TokenStore) : PagedListViewModel<Vide
|
|||
@Composable
|
||||
fun ChannelsScreen(
|
||||
onOpenChannel: (handle: String) -> Unit,
|
||||
viewModel: ChannelsViewModel = viewModel(factory = ChannelsViewModel.factory),
|
||||
epoch: Int = 0,
|
||||
viewModel: ChannelsViewModel = viewModel(key = "channels$epoch", factory = ChannelsViewModel.factory),
|
||||
) {
|
||||
LaunchedEffect(Unit) { viewModel.loadMore(force = true) }
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,8 @@ class RecentVideosViewModel(private val repository: PeerTubeRepository) : PagedL
|
|||
fun HomeRowsScreen(
|
||||
onOpenVideo: (VideoDto) -> Unit,
|
||||
restoreToken: Int = 0,
|
||||
viewModel: HomeRowsViewModel = viewModel(factory = HomeRowsViewModel.factory),
|
||||
epoch: Int = 0,
|
||||
viewModel: HomeRowsViewModel = viewModel(key = "home$epoch", factory = HomeRowsViewModel.factory),
|
||||
) {
|
||||
LaunchedEffect(Unit) { viewModel.load() }
|
||||
LaunchedEffect(restoreToken) { if (restoreToken > 0) viewModel.refresh() }
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.material.icons.filled.VideoLibrary
|
|||
import androidx.compose.material.icons.filled.PersonalVideo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
|
@ -47,6 +48,7 @@ enum class Destination(val title: String) {
|
|||
* focused card survive BACK (couch users don't want to re-navigate from the top). */
|
||||
@Composable
|
||||
fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
|
||||
val epoch by viewModel.sessionEpoch.collectAsState()
|
||||
var destination by rememberSaveable { mutableStateOf(Destination.Home) }
|
||||
var channelDetails by rememberSaveable { mutableStateOf<String?>(null) } // handle
|
||||
var playingVideo by rememberSaveable { mutableStateOf<String?>(null) } // shortUUID
|
||||
|
|
@ -64,6 +66,7 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
androidx.compose.runtime.key(epoch) {
|
||||
if (destination == Destination.Channels && channelDetails != null) {
|
||||
androidx.compose.foundation.layout.Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -75,6 +78,7 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
source = ListSource.Channel(channelDetails!!, channelDetails!!),
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
restoreToken = restoreToken,
|
||||
epoch = epoch,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -133,33 +137,41 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
) {
|
||||
when (destination) {
|
||||
Destination.Home ->
|
||||
HomeRowsScreen(onOpenVideo = { playingVideo = it.shortUUID })
|
||||
HomeRowsScreen(
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
epoch = epoch,
|
||||
)
|
||||
Destination.MyVideos ->
|
||||
VideoListScreen(
|
||||
source = ListSource.MyVideos,
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
restoreToken = restoreToken,
|
||||
epoch = epoch,
|
||||
)
|
||||
Destination.Channels -> ChannelsScreen(
|
||||
onOpenChannel = { handle -> channelDetails = handle },
|
||||
epoch = epoch,
|
||||
)
|
||||
Destination.Search ->
|
||||
VideoListScreen(
|
||||
source = ListSource.Search,
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
restoreToken = restoreToken,
|
||||
epoch = epoch,
|
||||
)
|
||||
Destination.Subscriptions ->
|
||||
VideoListScreen(
|
||||
source = ListSource.Subscriptions,
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
restoreToken = restoreToken,
|
||||
epoch = epoch,
|
||||
)
|
||||
Destination.History ->
|
||||
VideoListScreen(
|
||||
source = ListSource.History,
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
restoreToken = restoreToken,
|
||||
epoch = epoch,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -173,6 +185,7 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Destination.icon(): androidx.compose.ui.graphics.vector.ImageVector =
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
|
|
@ -41,7 +44,12 @@ fun SetupScreen(
|
|||
val password = rememberSaveable { mutableStateOf("") }
|
||||
val showPassword = rememberSaveable { mutableStateOf(false) }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val focusManager = androidx.compose.ui.platform.LocalFocusManager.current
|
||||
// Deterministic D-pad chain: DOWN from URL goes to Username, not Password
|
||||
// (geometric focus search misfired on device).
|
||||
val urlFocus = remember { FocusRequester() }
|
||||
val userFocus = remember { FocusRequester() }
|
||||
val passFocus = remember { FocusRequester() }
|
||||
val signInFocus = remember { FocusRequester() }
|
||||
val busy = loggingIn.collectAsState()
|
||||
val message = error.collectAsState()
|
||||
val localError = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
|
@ -67,11 +75,17 @@ fun SetupScreen(
|
|||
onValueChange = { url.value = it },
|
||||
label = { Text("PeerTube instance URL") },
|
||||
singleLine = true,
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||
imeAction = androidx.compose.ui.text.input.ImeAction.Next,
|
||||
),
|
||||
keyboardActions = androidx.compose.foundation.text.KeyboardActions(
|
||||
onNext = { userFocus.requestFocus() },
|
||||
),
|
||||
modifier = Modifier
|
||||
.focusRequester(urlFocus)
|
||||
.onPreviewKeyEvent { ke ->
|
||||
when (ke.key) {
|
||||
Key.DirectionDown -> { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down); true }
|
||||
Key.DirectionUp -> { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Up); true }
|
||||
Key.DirectionDown -> { userFocus.requestFocus(); true }
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -83,11 +97,18 @@ fun SetupScreen(
|
|||
onValueChange = { username.value = it },
|
||||
label = { Text("Username") },
|
||||
singleLine = true,
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||
imeAction = androidx.compose.ui.text.input.ImeAction.Next,
|
||||
),
|
||||
keyboardActions = androidx.compose.foundation.text.KeyboardActions(
|
||||
onNext = { passFocus.requestFocus() },
|
||||
),
|
||||
modifier = Modifier
|
||||
.focusRequester(userFocus)
|
||||
.onPreviewKeyEvent { ke ->
|
||||
when (ke.key) {
|
||||
Key.DirectionDown -> { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down); true }
|
||||
Key.DirectionUp -> { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Up); true }
|
||||
Key.DirectionDown -> { passFocus.requestFocus(); true }
|
||||
Key.DirectionUp -> { urlFocus.requestFocus(); true }
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -102,11 +123,14 @@ fun SetupScreen(
|
|||
visualTransformation =
|
||||
if (showPassword.value) androidx.compose.ui.text.input.VisualTransformation.None
|
||||
else androidx.compose.ui.text.input.PasswordVisualTransformation(),
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||
imeAction = androidx.compose.ui.text.input.ImeAction.Done,
|
||||
),
|
||||
modifier = Modifier
|
||||
.focusRequester(passFocus)
|
||||
.onPreviewKeyEvent { ke ->
|
||||
when (ke.key) {
|
||||
Key.DirectionDown -> { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down); true }
|
||||
Key.DirectionUp -> { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Up); true }
|
||||
Key.DirectionUp -> { userFocus.requestFocus(); true }
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,20 +42,21 @@ fun VideoListScreen(
|
|||
source: ListSource,
|
||||
onOpenVideo: (VideoDto) -> Unit,
|
||||
restoreToken: Int = 0,
|
||||
epoch: Int = 0,
|
||||
viewModel: VideoListViewModel = viewModel(
|
||||
key = sourceKey(source),
|
||||
key = "v$epoch:" + sourceKey(source),
|
||||
factory = VideoListViewModel.factory(source),
|
||||
),
|
||||
) {
|
||||
val items = viewModel.items
|
||||
// per-item focus requesters so BACK from the player can refocus the opened card
|
||||
val requesters = remember(items.size) {
|
||||
val requesters = androidx.compose.runtime.remember(items.size, epoch) {
|
||||
androidx.compose.runtime.mutableStateMapOf<Int, FocusRequester>()
|
||||
}
|
||||
// Pull initial focus out of the sidebar rail and into the content grid.
|
||||
val initialFocus = remember { FocusRequester() }
|
||||
// Which card was opened last — re-focus it when playback ends (UX report BUG-3).
|
||||
var lastOpenedIndex by androidx.compose.runtime.saveable.rememberSaveable { mutableStateOf(-1) }
|
||||
var lastOpenedIndex by androidx.compose.runtime.saveable.rememberSaveable(epoch) { mutableStateOf(-1) }
|
||||
LaunchedEffect(items.isNotEmpty(), source) {
|
||||
if (items.isNotEmpty()) runCatching { initialFocus.requestFocus() }
|
||||
}
|
||||
|
|
@ -72,7 +73,8 @@ fun VideoListScreen(
|
|||
}
|
||||
// Scroll position survives destination switches (per source), not just player exits.
|
||||
val gridState = androidx.compose.runtime.saveable.rememberSaveable(
|
||||
key = sourceKey(source), saver = androidx.compose.foundation.lazy.grid.LazyGridState.Saver,
|
||||
inputs = arrayOf(epoch),
|
||||
key = "g$epoch:" + sourceKey(source), saver = androidx.compose.foundation.lazy.grid.LazyGridState.Saver,
|
||||
) { androidx.compose.foundation.lazy.grid.LazyGridState() }
|
||||
|
||||
when (source) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue