File manager: up/back work from any start folder, and a ".." row to go up

Opening the manager at a folder (a game's files, its Proton prefix, the
saves folder, or the picker's remembered folder) ran openDrive() on the
start dir, which pinned the up/back floor to that folder: the toolbar
arrow was greyed out and Back closed the screen. The start dir now opens
with loadDirectory(), keeping the volume-root floor computed for it, and
folders in the app's private storage (internal Steam library, prefixes)
floor at the app's files dir instead of themselves.

Adds a ".." (Up one level) row at the top of the list and grid whenever
the current folder is above its floor, sharing one goUp() with the
toolbar arrow. Covers both screens built on FileManagerScreen: the File
Manager and the in-app file/folder picker. Strings for all 9 locales.
This commit is contained in:
The412Banner committed 2026-10-06 23:52:59 -04:00
1 parent fa99e2aab6
commit eda019a044
11 files changed
+78 -7

No files matched your search

@@ -22,6 +22,7 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.Checklist
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FileCopy
@@ -312,6 +313,47 @@ internal fun FileItemRow(
}
}
// The ".." row at the top of a folder: one level up, styled like a folder row so it reads as part
// of the list and takes controller focus the same way.
@Composable
internal fun ParentFolderRow(compact: Boolean = false, onTap: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 3.dp)
.clickable(onClick = onTap),
shape = RoundedCornerShape(10.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 12.dp, vertical = if (compact) 3.dp else 8.dp),
) {
Icon(
imageVector = Icons.Filled.ArrowUpward,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(if (compact) 24.dp else 36.dp),
)
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "..",
color = MaterialTheme.colorScheme.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
Text(
text = stringResource(R.string.fm_up_one_level),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
)
}
}
}
}
// Dedicated Favorites list that replaces the file list while the star toggle is on.
// Reads the store keyed on [favTick] so it recomposes after any pin/unpin.
@Composable
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -143,7 +144,14 @@ fun FileManagerScreen(
val name = abs.removePrefix("/storage/").substringBefore('/')
if (name.isNotEmpty() && name != "emulated" && name != "self") File("/storage/$name") else rootDir
}
else -> rootDir
// A game's own folder or Proton prefix lives in the app's private storage, which has no
// volume above it: floor at the app's files dir so up/back still reach the library roots.
else -> {
val appFiles = runCatching { context.filesDir.canonicalPath }.getOrNull()
generateSequence(rootDir) { it.parentFile }
.firstOrNull { appFiles != null && runCatching { it.canonicalPath }.getOrNull() == appFiles }
?: rootDir
}
}
mutableStateOf(vol)
}
@@ -232,7 +240,9 @@ fun FileManagerScreen(
loadDirectory(dir)
}
LaunchedEffect(Unit) { openDrive(rootDir) }
// Open the start dir without openDrive(): that pins the floor to the start dir itself, which
// greyed out up/back whenever the manager was opened at a game's folder.
LaunchedEffect(Unit) { loadDirectory(rootDir) }
// System/gesture Back: while the Favorites view is open it closes that first; otherwise
// it goes up one directory. Only at the current drive's root with Favorites closed is it
@@ -246,6 +256,14 @@ fun FileManagerScreen(
if (parent != null && parent.exists()) loadDirectory(parent)
}
// One level up, never above the current drive's root: shared by the toolbar arrow and the
// ".." row at the top of the list.
val canGoUp = currentDir != currentRoot && currentDir.parentFile?.exists() == true
fun goUp() {
val parent = currentDir.parentFile
if (currentDir != currentRoot && parent != null && parent.exists()) loadDirectory(parent)
}
// Resolve a non-colliding destination in [dir] for [name] (foo.txt -> "foo (1).txt").
fun uniqueDestination(dir: File, name: String): File {
var candidate = File(dir, name)
@@ -617,11 +635,7 @@ fun FileManagerScreen(
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 8.dp, vertical = 6.dp),
) {
IconButton(onClick = {
val parent = currentDir.parentFile
// Don't climb above the current drive's root.
if (currentDir != currentRoot && parent != null && parent.exists()) loadDirectory(parent)
}, enabled = currentDir != currentRoot) {
IconButton(onClick = { goUp() }, enabled = canGoUp) {
Icon(Icons.Filled.ArrowBack, stringResource(R.string.fm_back), tint = MaterialTheme.colorScheme.primary)
}
@@ -1044,6 +1058,9 @@ fun FileManagerScreen(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp),
) {
if (canGoUp) item(key = "..", span = { GridItemSpan(maxLineSpan) }) {
ParentFolderRow(compact = compactRows, onTap = { goUp() })
}
items(shownEntries, key = { it.absolutePath }) { file ->
val isFav = remember(file.absolutePath, favTick) {
FavoritesStore.isFavorite(context, file.absolutePath)
@@ -1109,6 +1126,9 @@ fun FileManagerScreen(
}
} else
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
if (canGoUp) item(key = "..") {
ParentFolderRow(compact = compactRows, onTap = { goUp() })
}
if (entries.isEmpty()) {
item {
Box(
+1
View File
@@ -1567,6 +1567,7 @@
<!-- %1$s is a folder name -->
<string name="fm_favorite_removed">Se quitó \"%1$s\" de Favoritos</string>
<string name="fm_empty_directory">Carpeta vacía</string>
<string name="fm_up_one_level">Subir un nivel</string>
<string name="fm_choose_folder">Elige una carpeta</string>
<string name="fm_choose_file">Elige un archivo</string>
+1
View File
@@ -946,4 +946,5 @@
<string name="game_file_folder">Choisir un dossier pour les fichiers du jeu frontal</string>
<string name="game_file_exported">Fichiers du jeu enregistrés dans %1$s</string>
<string name="game_frontend_files">Fichiers frontaux</string>
<string name="fm_up_one_level">Remonter d\'un niveau</string>
</resources>
+1
View File
@@ -1531,6 +1531,7 @@
<!-- %1$s is a folder name -->
<string name="fm_favorite_removed">「%1$s」をお気に入りから削除しました</string>
<string name="fm_empty_directory">空のフォルダ</string>
<string name="fm_up_one_level">1つ上の階層へ</string>
<string name="fm_choose_folder">フォルダを選択</string>
<string name="fm_choose_file">ファイルを選択</string>
+1
View File
@@ -1531,6 +1531,7 @@
<!-- %1$s is a folder name -->
<string name="fm_favorite_removed">\"%1$s\"을(를) 즐겨찾기에서 제거했습니다</string>
<string name="fm_empty_directory">빈 폴더</string>
<string name="fm_up_one_level">상위 폴더로</string>
<string name="fm_choose_folder">폴더 선택</string>
<string name="fm_choose_file">파일 선택</string>
+1
View File
@@ -954,4 +954,5 @@
<string name="game_file_folder">Выбрать папку для файлов игры фронтенда</string>
<string name="game_file_exported">Файлы игры сохранены в %1$s</string>
<string name="game_frontend_files">Файлы фронтенда</string>
<string name="fm_up_one_level">На уровень вверх</string>
</resources>
@@ -1531,6 +1531,7 @@
<!-- %1$s is a folder name -->
<string name="fm_favorite_removed">已将“%1$s”从收藏夹移除</string>
<string name="fm_empty_directory">空文件夹</string>
<string name="fm_up_one_level">返回上一级</string>
<string name="fm_choose_folder">选择文件夹</string>
<string name="fm_choose_file">选择文件</string>
@@ -1531,6 +1531,7 @@
<!-- %1$s is a folder name -->
<string name="fm_favorite_removed">已將「%1$s」從我的最愛移除</string>
<string name="fm_empty_directory">空白目錄</string>
<string name="fm_up_one_level">返回上一層</string>
<string name="fm_choose_folder">選擇資料夾</string>
<string name="fm_choose_file">選擇檔案</string>
@@ -1531,6 +1531,7 @@
<!-- %1$s is a folder name -->
<string name="fm_favorite_removed">已將「%1$s」從我的最愛移除</string>
<string name="fm_empty_directory">空的資料夾</string>
<string name="fm_up_one_level">返回上一層</string>
<string name="fm_choose_folder">選擇資料夾</string>
<string name="fm_choose_file">選擇檔案</string>
+1
View File
@@ -1481,6 +1481,7 @@
<!-- %1$s is a folder name -->
<string name="fm_favorite_removed">Removed \"%1$s\" from Favorites</string>
<string name="fm_empty_directory">Empty directory</string>
<string name="fm_up_one_level">Up one level</string>
<string name="fm_choose_folder">Choose a folder</string>
<string name="fm_choose_file">Choose a file</string>
<!-- On-screen controller tint colors -->