From 52a85e5aa2df8ba5f795ff4b82ad69141a1d4ecf Mon Sep 17 00:00:00 2001 From: David Date: Thu, 16 Jul 2026 09:00:03 -0500 Subject: [PATCH 1/2] Deps: upgraded to latest - Android Gradle Plugin: 9.2.1 - JUnit: 6.1.2 - Kotlin: 2.4.10 --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f65df16a0..563997e9d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] activityCompose = "1.13.0" -agp = "9.2.1" +agp = "9.3.0" androidJunit5 = "2.0.1" annotation = "1.10.0" assertjCore = "3.27.7" @@ -14,10 +14,10 @@ handoff = "0.30.3" jodaTime = "2.14.2" jsoup = "1.22.2" junit = "4.13.2" -junitJupiterApi = "6.1.0" +junitJupiterApi = "6.1.2" junitKtx = "1.3.0" kotlinxCoroutinesRx3 = "1.11.0" -kotlin = "2.4.0" +kotlin = "2.4.10" ktorVersion = "3.5.1" lazyJ = "0.11.0" lifecycle = "2.11.0" From 227f7bca6f50fb679feaf69fa2917d101d8057b6 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 17 Jul 2026 08:01:46 -0500 Subject: [PATCH 2/2] UI,NowPlaying: Use lazyListState on non-touch UIs --- .../view/NavigateToTvLibraryDestination.kt | 113 -------------- .../nowplaying/view/NowPlayingPlaylist.kt | 59 ++++---- .../nowplaying/view/NowPlayingTvPlaylist.kt | 141 ------------------ .../nowplaying/view/NowPlayingView.kt | 5 +- 4 files changed, 33 insertions(+), 285 deletions(-) delete mode 100644 projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NavigateToTvLibraryDestination.kt delete mode 100644 projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingTvPlaylist.kt diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NavigateToTvLibraryDestination.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NavigateToTvLibraryDestination.kt deleted file mode 100644 index bd1012080..000000000 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NavigateToTvLibraryDestination.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.lasthopesoftware.bluewater.client.playback.nowplaying.view - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import com.lasthopesoftware.bluewater.android.ui.ScreenDimensionsScope -import com.lasthopesoftware.bluewater.client.browsing.ScopedViewModelDependencies -import com.lasthopesoftware.bluewater.client.browsing.files.list.search.SearchFilesView -import com.lasthopesoftware.bluewater.client.browsing.items.list.ConnectionLostView -import com.lasthopesoftware.bluewater.client.browsing.items.list.LoadedItemListView -import com.lasthopesoftware.bluewater.client.browsing.items.list.LoadedItemListScreen -import com.lasthopesoftware.bluewater.client.browsing.navigation.BrowserLibraryDestination -import com.lasthopesoftware.bluewater.client.browsing.navigation.DownloadsScreen -import com.lasthopesoftware.bluewater.client.browsing.navigation.FilePropertySearchScreen -import com.lasthopesoftware.bluewater.client.browsing.navigation.ItemScreen -import com.lasthopesoftware.bluewater.client.browsing.navigation.LibraryScreen -import com.lasthopesoftware.bluewater.client.browsing.navigation.SearchScreen -import com.lasthopesoftware.bluewater.client.connection.ConnectionLostExceptionFilter -import com.lasthopesoftware.bluewater.client.stored.library.items.files.view.ActiveFileDownloadsView -import com.lasthopesoftware.bluewater.shared.android.viewmodels.ViewModelInitAction -import com.lasthopesoftware.promises.extensions.suspend -import java.io.IOException - -@Composable -fun ScreenDimensionsScope.NavigateToTvLibraryDestination(destination: BrowserLibraryDestination, browserViewDependencies: ScopedViewModelDependencies) { - when (destination) { - is LibraryScreen -> { - LoadedItemListScreen(browserViewDependencies, destination.libraryId, null) - } - - is ItemScreen -> { - LoadedItemListScreen(browserViewDependencies, destination.libraryId, destination.item) - } - - is DownloadsScreen -> { - with(browserViewDependencies) { - ActiveFileDownloadsView( - activeFileDownloadsViewModel = activeFileDownloadsViewModel, - trackHeadlineViewModelProvider = reusableFileItemViewModelProvider, - applicationNavigation = applicationNavigation, - ) - - activeFileDownloadsViewModel.loadActiveDownloads(destination.libraryId) - } - } - - is FilePropertySearchScreen, is SearchScreen -> { - var isConnectionLost by remember { mutableStateOf(false) } - var reinitializeConnection by remember { mutableStateOf(false) } - - browserViewDependencies.apply { - if (isConnectionLost) { - ConnectionLostView( - onCancel = { applicationNavigation.viewApplicationSettings() }, - onRetry = { - reinitializeConnection = true - } - ) - } else { - SearchFilesView( - searchFilesViewModel = searchFilesViewModel, - nowPlayingViewModel = nowPlayingFilePropertiesViewModel, - trackHeadlineViewModelProvider = reusablePlaylistFileItemViewModelProvider, - itemListMenuBackPressedHandler = itemListMenuBackPressedHandler, - applicationNavigation = applicationNavigation, - playbackServiceController = playbackServiceController, - stringResources = stringResources, - backStackBuilder = undoBackStackBuilder, - ) - } - - ViewModelInitAction { - searchFilesViewModel.setActiveLibraryId(destination.libraryId) - - if (reinitializeConnection) { - LaunchedEffect(key1 = Unit) { - isConnectionLost = - !connectionStatusViewModel.initializeConnection(destination.libraryId).suspend() - reinitializeConnection = false - } - } - - if (!isConnectionLost) { - LaunchedEffect(destination) { - try { - when (destination) { - is FilePropertySearchScreen -> { - destination.filePropertyFilter?.let(searchFilesViewModel::prependFilter) - } - is SearchScreen -> { - searchFilesViewModel.query.value = destination.searchQuery - } - } - - searchFilesViewModel.findFiles().suspend() - } catch (e: IOException) { - if (ConnectionLostExceptionFilter.isConnectionLostException(e)) - isConnectionLost = true - else - applicationNavigation.backOut().suspend() - } catch (_: Exception) { - applicationNavigation.backOut().suspend() - } - } - } - } - } - } - } -} diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingPlaylist.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingPlaylist.kt index cd0050b5f..0710025ed 100644 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingPlaylist.kt +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingPlaylist.kt @@ -10,11 +10,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -43,9 +41,9 @@ import com.lasthopesoftware.bluewater.shared.android.UndoStack import com.lasthopesoftware.bluewater.shared.android.messages.ViewModelMessageBus import com.lasthopesoftware.bluewater.shared.android.viewmodels.PooledCloseablesViewModel import com.lasthopesoftware.bluewater.shared.messages.registerReceiver +import com.lasthopesoftware.observables.mapNotNull import com.lasthopesoftware.observables.subscribeAsState import com.lasthopesoftware.promises.extensions.toPromise -import kotlinx.coroutines.launch @Composable fun DragDropItemScope.NowPlayingFileView( @@ -305,41 +303,31 @@ fun NowPlayingPlaylist( val nowPlayingFiles by playlistViewModel.nowPlayingList.subscribeAsState() val activeLibraryId by nowPlayingFilePropertiesViewModel.activeLibraryId.subscribeAsState() - val dragDropListState = rememberDragDropListState( - lazyListState = lazyListState, - onMove = { from, to -> - playlistViewModel.swapFiles(from, to) - }, - onDragEnd = { from, to -> - activeLibraryId?.also { - playbackServiceController.moveFile(it, from, to) - } - } - ) - - val playingFile by nowPlayingFilePropertiesViewModel.nowPlayingFile.subscribeAsState() - val isAutoScrollEnabled by playlistViewModel.isAutoScrolling.subscribeAsState() if (isAutoScrollEnabled) { var lastScrolledToItem by rememberSaveable { mutableStateOf(null) } - LaunchedEffect(key1 = playingFile) { - playingFile?.also { - if (it != lastScrolledToItem) { - dragDropListState.lazyListState.scrollToFileIfNotScrolling(it) - lastScrolledToItem = it + DisposableEffect(key1 = nowPlayingFilePropertiesViewModel, key2 = lazyListState) { + val sub = nowPlayingFilePropertiesViewModel + .nowPlayingFile + .mapNotNull() + .forEach { + if (it != lastScrolledToItem) { + lazyListState.requestScrollToFileIfNotScrolling(it) + lastScrolledToItem = it + } } + + onDispose { + sub.dispose() } } } - val scope = rememberCoroutineScope() - DisposableEffect(key1 = Unit) { + DisposableEffect(key1 = viewModelMessageBus, key2 = lazyListState, key3 = nowPlayingFilePropertiesViewModel) { val registration = viewModelMessageBus.registerReceiver { _: NowPlayingMessage.ScrollToNowPlaying -> - scope.launch { - playingFile?.apply { - dragDropListState.lazyListState.scrollToItem(playlistPosition) - } + nowPlayingFilePropertiesViewModel.nowPlayingFile.value?.also { + lazyListState.requestScrollToFileIfNotScrolling(it) } } @@ -348,8 +336,21 @@ fun NowPlayingPlaylist( } } + val playingFile by nowPlayingFilePropertiesViewModel.nowPlayingFile.subscribeAsState() val inputMode = LocalInputModeManager.current if (inputMode.inputMode == InputMode.Touch) { + val dragDropListState = rememberDragDropListState( + lazyListState = lazyListState, + onMove = { from, to -> + playlistViewModel.swapFiles(from, to) + }, + onDragEnd = { from, to -> + activeLibraryId?.also { + playbackServiceController.moveFile(it, from, to) + } + } + ) + DragDropLazyColumn( dragDropListState = dragDropListState, modifier = modifier, @@ -369,7 +370,7 @@ fun NowPlayingPlaylist( } } } else { - LazyColumn(modifier = modifier) { + LazyColumn(modifier = modifier, state = lazyListState) { items(items = nowPlayingFiles, key = { it }) { f -> NowPlayingFileView( activeLibraryId = activeLibraryId, diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingTvPlaylist.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingTvPlaylist.kt deleted file mode 100644 index ccea6da26..000000000 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingTvPlaylist.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.lasthopesoftware.bluewater.client.playback.nowplaying.view - -import androidx.compose.foundation.focusGroup -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import com.lasthopesoftware.bluewater.NavigateApplication -import com.lasthopesoftware.bluewater.client.browsing.files.list.ViewPlaylistFileItem -import com.lasthopesoftware.bluewater.client.playback.file.PositionedFile -import com.lasthopesoftware.bluewater.client.playback.nowplaying.view.components.NowPlayingItemView -import com.lasthopesoftware.bluewater.client.playback.nowplaying.view.viewmodels.NowPlayingFilePropertiesViewModel -import com.lasthopesoftware.bluewater.client.playback.nowplaying.view.viewmodels.playlist.NowPlayingPlaylistViewModel -import com.lasthopesoftware.bluewater.client.playback.service.ControlPlaybackService -import com.lasthopesoftware.bluewater.shared.android.messages.ViewModelMessageBus -import com.lasthopesoftware.bluewater.shared.android.viewmodels.PooledCloseablesViewModel -import com.lasthopesoftware.bluewater.shared.messages.registerReceiver -import com.lasthopesoftware.observables.subscribeAsState -import kotlinx.coroutines.launch - -@Composable -fun NowPlayingTvPlaylist( - childItemViewModelProvider: PooledCloseablesViewModel, - nowPlayingFilePropertiesViewModel: NowPlayingFilePropertiesViewModel, - applicationNavigation: NavigateApplication, - playbackServiceController: ControlPlaybackService, - playlistViewModel: NowPlayingPlaylistViewModel, - viewModelMessageBus: ViewModelMessageBus, - modifier: Modifier = Modifier, -) { - val nowPlayingFiles by playlistViewModel.nowPlayingList.subscribeAsState() - val activeLibraryId by nowPlayingFilePropertiesViewModel.activeLibraryId.subscribeAsState() - - val lazyListState = rememberLazyListState() - - val playingFile by nowPlayingFilePropertiesViewModel.nowPlayingFile.subscribeAsState() - - val isAutoScrollEnabled by playlistViewModel.isAutoScrolling.subscribeAsState() - if (isAutoScrollEnabled) { - LaunchedEffect(key1 = playingFile) { - playingFile?.apply { - if (!lazyListState.isScrollInProgress) - lazyListState.scrollToItem(playlistPosition) - } - } - } - - val scope = rememberCoroutineScope() - DisposableEffect(key1 = Unit) { - val registration = - viewModelMessageBus.registerReceiver { _: NowPlayingMessage.ScrollToNowPlaying -> - scope.launch { - playingFile?.apply { - lazyListState.scrollToItem(playlistPosition) - } - } - } - - onDispose { - registration.close() - } - } - - val isEditing by playlistViewModel.isEditingPlaylist.subscribeAsState() - - @Composable - fun NowPlayingFileView(positionedFile: PositionedFile) { - val fileItemViewModel = remember(childItemViewModelProvider::getViewModel) - - DisposableEffect(activeLibraryId, positionedFile) { - activeLibraryId?.also { - fileItemViewModel.promiseUpdate(it, positionedFile.serviceFile) - } - - onDispose { - fileItemViewModel.reset() - } - } - - val fileName by fileItemViewModel.title.subscribeAsState() - val artist by fileItemViewModel.artist.subscribeAsState() - val isPlaying by remember { derivedStateOf { playingFile == positionedFile } } - - NowPlayingItemView( - itemName = fileName, - artist = artist, - isActive = isPlaying, - isEditingPlaylist = isEditing, - onMoveItemUp = { - activeLibraryId?.let { - val currentPosition = positionedFile.playlistPosition - val newPosition = currentPosition - 1 - playlistViewModel.swapFiles(currentPosition, newPosition) - playbackServiceController.moveFile(it, currentPosition, newPosition) - } - }, - onMoveItemDown = { - activeLibraryId?.let { - val currentPosition = positionedFile.playlistPosition - val newPosition = currentPosition + 1 - playlistViewModel.swapFiles(currentPosition, newPosition) - playbackServiceController.moveFile(it, currentPosition, newPosition) - } - }, - onItemClick = { - activeLibraryId?.also { - applicationNavigation.viewNowPlayingFileDetails(it, positionedFile) - } - }, - onRemoveFromNowPlayingClick = { - activeLibraryId?.also { - playbackServiceController - .removeFromPlaylistAtPosition(it, positionedFile.playlistPosition) - } - } - ) - } - - LazyColumn( - state = lazyListState, - modifier = Modifier - .focusGroup() - .onFocusChanged { f -> - if (f.hasFocus) playlistViewModel.lockOutAutoScroll() - else playlistViewModel.releaseAutoScroll() - } - .then(modifier), - ) { - items(items = nowPlayingFiles, key = { f -> f }) { f -> - NowPlayingFileView(positionedFile = f) - } - } -} diff --git a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingView.kt b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingView.kt index 85def7150..fd3586310 100644 --- a/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingView.kt +++ b/projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/NowPlayingView.kt @@ -397,9 +397,9 @@ fun NowPlayingControls( } } -suspend fun LazyListState.scrollToFileIfNotScrolling(file: PositionedFile) { +fun LazyListState.requestScrollToFileIfNotScrolling(file: PositionedFile) { if (!isScrollInProgress) - scrollToItem(file.playlistPosition) + requestScrollToItem(file.playlistPosition) } private val collapsedControlsHeight = ProgressIndicatorDefaults.StrokeWidth + Dimensions.appBarHeight @@ -942,6 +942,7 @@ fun BoxWithConstraintsScope.NowPlayingWideView( } onDispose { + playlistViewModel.disableUserAutoScrolling() playlistViewModel.disableSystemAutoScrolling() } }