From 1823d8651694d3ba13217606278233fe0c399be8 Mon Sep 17 00:00:00 2001 From: Ayaan Date: Wed, 24 Jun 2026 13:02:25 +0530 Subject: [PATCH 1/2] fix: enable BackHandler for album selection in LibraryScreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Changed isAlbumSelectionMode to use derivedStateOf to ensure it is correctly tracked as a Compose state. • This allows hasSelectionInCurrentTab to react to album selection changes, properly enabling the BackHandler. • Fixed a bug where pressing the system back button while in album selection mode would close the app instead of clearing the selection. --- .../theveloper/pixelplay/presentation/screens/LibraryScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt index 5d37d029e..8bceb22cf 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt @@ -522,7 +522,7 @@ fun LibraryScreen( var showMultiSelectionSheet by remember { mutableStateOf(false) } var selectedAlbums by remember { mutableStateOf>(emptyList()) } val selectedAlbumIds = remember(selectedAlbums) { selectedAlbums.map { it.id }.toSet() } - val isAlbumSelectionMode = selectedAlbums.isNotEmpty() + val isAlbumSelectionMode by remember { derivedStateOf { selectedAlbums.isNotEmpty() } } var showAlbumMultiSelectionSheet by remember { mutableStateOf(false) } var showBatchEditSheet by remember { mutableStateOf(false) } From dc5664add400114018e13f046c20aa0556d139da Mon Sep 17 00:00:00 2001 From: Ayaan Date: Thu, 25 Jun 2026 05:17:16 +0530 Subject: [PATCH 2/2] feat: add tab switcher to exclude directories selection in setup/settings --- .../components/FileExplorerBottomSheet.kt | 120 +++++++++++++++--- .../screens/SettingsCategoryScreen.kt | 2 + .../presentation/screens/SetupScreen.kt | 4 + .../viewmodel/FileExplorerStateHolder.kt | 41 ++++++ .../viewmodel/SettingsViewModel.kt | 1 + .../presentation/viewmodel/SetupViewModel.kt | 1 + 6 files changed, 151 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt index e1aed1b4c..137e71c8f 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/FileExplorerBottomSheet.kt @@ -68,6 +68,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -94,12 +95,17 @@ import com.theveloper.pixelplay.ui.theme.LocalShowScrollbar import com.theveloper.pixelplay.utils.StorageInfo import java.io.File +enum class FileExplorerTab { + COLLAPSED, EXPANDED +} + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun FileExplorerDialog( visible: Boolean, currentPath: File, directoryChildren: List, + flatDirectoryChildren: List, availableStorages: List, selectedStorageIndex: Int, isLoading: Boolean, @@ -147,6 +153,7 @@ fun FileExplorerDialog( FileExplorerContent( currentPath = currentPath, directoryChildren = directoryChildren, + flatDirectoryChildren = flatDirectoryChildren, availableStorages = availableStorages, selectedStorageIndex = selectedStorageIndex, isLoading = isLoading, @@ -176,6 +183,7 @@ fun FileExplorerDialog( fun FileExplorerContent( currentPath: File, directoryChildren: List, + flatDirectoryChildren: List, availableStorages: List, selectedStorageIndex: Int, isLoading: Boolean, @@ -215,6 +223,17 @@ fun FileExplorerContent( isLoading || isPriming || !isReady || !isCurrentDirectoryResolved ) } + var currentTab by remember { mutableStateOf(FileExplorerTab.COLLAPSED) } + val showExpandedLoadingState = remember( + flatDirectoryChildren, + isLoading, + isPriming, + isReady + ) { + flatDirectoryChildren.isEmpty() && ( + isLoading || isPriming || !isReady + ) + } val loadingMessage = remember(isPriming, isReady) { if (isPriming || !isReady) { "Preparing folders…" @@ -303,11 +322,25 @@ fun FileExplorerContent( .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(7.2.dp) ) { + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.file_explorer_hint), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .padding(horizontal = 18.dp) + ) + // Only show storage tabs if there's more than one storage if (availableStorages.size > 1) { PrimaryTabRow( modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .padding(horizontal = 18.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(5.dp), selectedTabIndex = safeSelectedStorageIndex, containerColor = Color.Transparent, indicator = { @@ -339,25 +372,64 @@ fun FileExplorerContent( } } } - - Text( - text = stringResource(R.string.file_explorer_hint), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + PrimaryTabRow( modifier = Modifier - .padding(top = 10.dp) + .fillMaxWidth() .padding(horizontal = 18.dp) - ) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(5.dp), + selectedTabIndex = currentTab.ordinal, + containerColor = Color.Transparent, + indicator = { + TabRowDefaults.PrimaryIndicator( + modifier = Modifier.tabIndicatorOffset( + selectedTabIndex = currentTab.ordinal, + matchContentSize = true + ), + height = 3.dp, + color = Color.Transparent + ) + }, + divider = {} + ) { + TabAnimation( + index = 0, + title = "Folders", + selectedIndex = currentTab.ordinal, + onClick = { currentTab = FileExplorerTab.COLLAPSED } + ) { + Text( + text = "Folders", + fontWeight = FontWeight.Bold, + maxLines = 1, + fontFamily = GoogleSansRounded + ) + } + TabAnimation( + index = 1, + title = "All subfolders", + selectedIndex = currentTab.ordinal, + onClick = { currentTab = FileExplorerTab.EXPANDED } + ) { + Text( + text = "All subfolders", + fontWeight = FontWeight.Bold, + maxLines = 1, + fontFamily = GoogleSansRounded + ) + } + } FileExplorerHeader( modifier = Modifier.padding(horizontal = 18.dp), - currentPath = currentPath, + currentPath = if (currentTab == FileExplorerTab.COLLAPSED) currentPath else rootDirectory, rootDirectory = rootDirectory, - isAtRoot = isAtRoot, + isAtRoot = if (currentTab == FileExplorerTab.COLLAPSED) isAtRoot else true, onNavigateUp = onNavigateUp, onNavigateHome = onNavigateHome, onNavigateTo = onNavigateTo, - navigationEnabled = true + navigationEnabled = currentTab == FileExplorerTab.COLLAPSED ) Box( @@ -367,17 +439,23 @@ fun FileExplorerContent( ) { AnimatedContent( targetState = Triple( - currentPath, - directoryChildren, - listOf(isLoading, isPriming, isReady, isCurrentDirectoryResolved) + currentTab, + if (currentTab == FileExplorerTab.COLLAPSED) directoryChildren else flatDirectoryChildren, + if (currentTab == FileExplorerTab.COLLAPSED) currentPath else null ), label = "directory_content", transitionSpec = { fadeIn(animationSpec = tween(220)) togetherWith fadeOut(animationSpec = tween(200)) } - ) { (_, children, _) -> + ) { (tab, children, _) -> + val showLoading = if (tab == FileExplorerTab.COLLAPSED) { + showLoadingState + } else { + showExpandedLoadingState + } + when { - showLoadingState -> ExplorerLoadingState( + showLoading -> ExplorerLoadingState( message = loadingMessage, supportingText = loadingHint ) @@ -412,7 +490,7 @@ fun FileExplorerContent( isBlocked = directoryEntry.isBlocked, onNavigate = { onNavigateTo(directoryEntry.file) }, onToggleAllowed = { onToggleAllowed(directoryEntry.file) }, - navigationEnabled = true + navigationEnabled = tab == FileExplorerTab.COLLAPSED ) } item { Spacer(modifier = Modifier.height(76.dp)) } @@ -546,7 +624,13 @@ private fun FileExplorerItem( .fillMaxWidth() .clip(shape) .background(containerColor) - .clickable(enabled = navigationEnabled) { onNavigate() } + .clickable { + if (navigationEnabled) { + onNavigate() + } else { + onToggleAllowed() + } + } .padding(horizontal = 14.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt index dd1672dbc..544cf66d2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt @@ -216,6 +216,7 @@ fun SettingsCategoryScreen( val aiProvider by settingsViewModel.aiProvider.collectAsStateWithLifecycle() val currentPath by settingsViewModel.currentPath.collectAsStateWithLifecycle() val directoryChildren by settingsViewModel.currentDirectoryChildren.collectAsStateWithLifecycle() + val flatDirectoryChildren by settingsViewModel.flatDirectoryChildren.collectAsStateWithLifecycle(initialValue = emptyList()) val availableStorages by settingsViewModel.availableStorages.collectAsStateWithLifecycle() val selectedStorageIndex by settingsViewModel.selectedStorageIndex.collectAsStateWithLifecycle() val isLoadingDirectories by settingsViewModel.isLoadingDirectories.collectAsStateWithLifecycle() @@ -1453,6 +1454,7 @@ fun SettingsCategoryScreen( visible = showExplorerSheet, currentPath = currentPath, directoryChildren = directoryChildren, + flatDirectoryChildren = flatDirectoryChildren, availableStorages = availableStorages, selectedStorageIndex = selectedStorageIndex, isLoading = isLoadingDirectories, diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt index 510010631..b759b5624 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/SetupScreen.kt @@ -173,6 +173,7 @@ fun SetupScreen( val uiState by setupViewModel.uiState.collectAsStateWithLifecycle() val currentPath by setupViewModel.currentPath.collectAsStateWithLifecycle() val directoryChildren by setupViewModel.currentDirectoryChildren.collectAsStateWithLifecycle() + val flatDirectoryChildren by setupViewModel.flatDirectoryChildren.collectAsStateWithLifecycle(initialValue = emptyList()) val availableStorages by setupViewModel.availableStorages.collectAsStateWithLifecycle() val selectedStorageIndex by setupViewModel.selectedStorageIndex.collectAsStateWithLifecycle() val isExplorerPriming by setupViewModel.isExplorerPriming.collectAsStateWithLifecycle() @@ -346,6 +347,7 @@ fun SetupScreen( uiState = uiState, currentPath = currentPath, directoryChildren = directoryChildren, + flatDirectoryChildren = flatDirectoryChildren, availableStorages = availableStorages, selectedStorageIndex = selectedStorageIndex, isExplorerPriming = isExplorerPriming, @@ -452,6 +454,7 @@ fun DirectorySelectionPage( uiState: SetupUiState, currentPath: File, directoryChildren: List, + flatDirectoryChildren: List, availableStorages: List, selectedStorageIndex: Int, isExplorerPriming: Boolean, @@ -511,6 +514,7 @@ fun DirectorySelectionPage( visible = showDirectoryPicker, currentPath = currentPath, directoryChildren = directoryChildren, + flatDirectoryChildren = flatDirectoryChildren, availableStorages = availableStorages, selectedStorageIndex = selectedStorageIndex, isLoading = uiState.isLoadingDirectories, diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt index b43ff8724..8d12893e2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/FileExplorerStateHolder.kt @@ -125,6 +125,10 @@ class FileExplorerStateHolder( private val _currentDirectoryChildren = MutableStateFlow>(emptyList()) val currentDirectoryChildren: StateFlow> = _currentDirectoryChildren.asStateFlow() + private val _rawFlatDirectoryChildren = MutableStateFlow>(emptyList()) + private val _flatDirectoryChildren = MutableStateFlow>(emptyList()) + val flatDirectoryChildren: StateFlow> = _flatDirectoryChildren.asStateFlow() + private val mapperDispatcher = Dispatchers.Default private val prefetchDispatcher = Dispatchers.IO.limitedParallelism(2) private val loadMutex = Mutex() @@ -182,6 +186,31 @@ class FileExplorerStateHolder( _currentDirectoryChildren.value = it }.launchIn(scope) + combine( + _rawFlatDirectoryChildren, + _allowedDirectories, + _blockedDirectories + ) { rawEntries, allowed, blocked -> + Triple(rawEntries, allowed, blocked) + } + .mapLatest { (rawEntries, allowed, blocked) -> + val resolver = DirectoryRuleResolver(allowed, blocked) + rawEntries.map { raw -> + DirectoryEntry( + file = raw.file, + directAudioCount = raw.directAudioCount, + totalAudioCount = raw.totalAudioCount, + canonicalPath = raw.canonicalPath, + displayName = raw.displayName, + isBlocked = resolver.isBlocked(raw.canonicalPath) + ) + } + } + .flowOn(mapperDispatcher) + .onEach { + _flatDirectoryChildren.value = it + }.launchIn(scope) + } fun refreshAvailableStorages() { @@ -331,6 +360,7 @@ class FileExplorerStateHolder( prefetchedDirectoryKeys.clear() resolvedDirectoryKeys.clear() mediaStoreDirectoryIndex = null + _rawFlatDirectoryChildren.value = emptyList() } if (updatePath) { @@ -548,6 +578,17 @@ class FileExplorerStateHolder( } } + val rawFlatEntries = directAudioCountByPath.map { (path, directCount) -> + val totalCount = totalAudioCountByPath[path] ?: directCount + RawDirectoryEntry( + file = File(path), + directAudioCount = directCount, + totalAudioCount = totalCount, + canonicalPath = path + ) + }.sortedWith(compareBy { it.file.name.lowercase() }) + _rawFlatDirectoryChildren.value = rawFlatEntries + MediaStoreDirectoryIndex( childrenByParent = childrenByParent.mapValues { it.value.toSet() }, directAudioCountByPath = directAudioCountByPath.toMap(), diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt index abba7eace..16ab2aa7a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt @@ -530,6 +530,7 @@ class SettingsViewModel @Inject constructor( val currentPath = fileExplorerStateHolder.currentPath val currentDirectoryChildren = fileExplorerStateHolder.currentDirectoryChildren + val flatDirectoryChildren = fileExplorerStateHolder.flatDirectoryChildren val blockedDirectories = fileExplorerStateHolder.blockedDirectories val availableStorages = fileExplorerStateHolder.availableStorages val selectedStorageIndex = fileExplorerStateHolder.selectedStorageIndex diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SetupViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SetupViewModel.kt index 156e196ee..99978826d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SetupViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SetupViewModel.kt @@ -86,6 +86,7 @@ class SetupViewModel @Inject constructor( val currentPath = fileExplorerStateHolder.currentPath val currentDirectoryChildren = fileExplorerStateHolder.currentDirectoryChildren + val flatDirectoryChildren = fileExplorerStateHolder.flatDirectoryChildren val blockedDirectories = fileExplorerStateHolder.blockedDirectories val availableStorages = fileExplorerStateHolder.availableStorages val selectedStorageIndex = fileExplorerStateHolder.selectedStorageIndex