Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DirectoryEntry>,
flatDirectoryChildren: List<DirectoryEntry>,
availableStorages: List<StorageInfo>,
selectedStorageIndex: Int,
isLoading: Boolean,
Expand Down Expand Up @@ -147,6 +153,7 @@ fun FileExplorerDialog(
FileExplorerContent(
currentPath = currentPath,
directoryChildren = directoryChildren,
flatDirectoryChildren = flatDirectoryChildren,
availableStorages = availableStorages,
selectedStorageIndex = selectedStorageIndex,
isLoading = isLoading,
Expand Down Expand Up @@ -176,6 +183,7 @@ fun FileExplorerDialog(
fun FileExplorerContent(
currentPath: File,
directoryChildren: List<DirectoryEntry>,
flatDirectoryChildren: List<DirectoryEntry>,
availableStorages: List<StorageInfo>,
selectedStorageIndex: Int,
isLoading: Boolean,
Expand Down Expand Up @@ -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…"
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(
Expand All @@ -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
)
Expand Down Expand Up @@ -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)) }
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ fun LibraryScreen(
var showMultiSelectionSheet by remember { mutableStateOf(false) }
var selectedAlbums by remember { mutableStateOf<List<Album>>(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) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -1453,6 +1454,7 @@ fun SettingsCategoryScreen(
visible = showExplorerSheet,
currentPath = currentPath,
directoryChildren = directoryChildren,
flatDirectoryChildren = flatDirectoryChildren,
availableStorages = availableStorages,
selectedStorageIndex = selectedStorageIndex,
isLoading = isLoadingDirectories,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -346,6 +347,7 @@ fun SetupScreen(
uiState = uiState,
currentPath = currentPath,
directoryChildren = directoryChildren,
flatDirectoryChildren = flatDirectoryChildren,
availableStorages = availableStorages,
selectedStorageIndex = selectedStorageIndex,
isExplorerPriming = isExplorerPriming,
Expand Down Expand Up @@ -452,6 +454,7 @@ fun DirectorySelectionPage(
uiState: SetupUiState,
currentPath: File,
directoryChildren: List<DirectoryEntry>,
flatDirectoryChildren: List<DirectoryEntry>,
availableStorages: List<StorageInfo>,
selectedStorageIndex: Int,
isExplorerPriming: Boolean,
Expand Down Expand Up @@ -511,6 +514,7 @@ fun DirectorySelectionPage(
visible = showDirectoryPicker,
currentPath = currentPath,
directoryChildren = directoryChildren,
flatDirectoryChildren = flatDirectoryChildren,
availableStorages = availableStorages,
selectedStorageIndex = selectedStorageIndex,
isLoading = uiState.isLoadingDirectories,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ class FileExplorerStateHolder(
private val _currentDirectoryChildren = MutableStateFlow<List<DirectoryEntry>>(emptyList())
val currentDirectoryChildren: StateFlow<List<DirectoryEntry>> = _currentDirectoryChildren.asStateFlow()

private val _rawFlatDirectoryChildren = MutableStateFlow<List<RawDirectoryEntry>>(emptyList())
private val _flatDirectoryChildren = MutableStateFlow<List<DirectoryEntry>>(emptyList())
val flatDirectoryChildren: StateFlow<List<DirectoryEntry>> = _flatDirectoryChildren.asStateFlow()

private val mapperDispatcher = Dispatchers.Default
private val prefetchDispatcher = Dispatchers.IO.limitedParallelism(2)
private val loadMutex = Mutex()
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -331,6 +360,7 @@ class FileExplorerStateHolder(
prefetchedDirectoryKeys.clear()
resolvedDirectoryKeys.clear()
mediaStoreDirectoryIndex = null
_rawFlatDirectoryChildren.value = emptyList()
}

if (updatePath) {
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading