From a74256499a29db8d18b003d2949d5214567bcef4 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Wed, 29 Jul 2026 13:47:12 +0000 Subject: [PATCH 1/3] Persist drives created inside Wine across container boots dosdevices is now the source of truth for drive mappings. The boot rebuild no longer deletes every drive letter before recreating them from the container config, and bind paths are derived from the prefix, so a drive added in winecfg is exposed to the guest instead of being dropped. The container config seeds only a prefix that has no drives yet, which keeps defaults working on a fresh container and restores them if the prefix is ever rebuilt. --- .../ContainerSettingsComposeDialog.kt | 9 +- .../display/XServerDisplayActivity.java | 3 +- app/src/main/runtime/wine/WineUtils.java | 128 ++++++++++++------ 3 files changed, 96 insertions(+), 44 deletions(-) diff --git a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt index 8704af95c..c9597b386 100644 --- a/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt +++ b/app/src/main/feature/settings/containers/ContainerSettingsComposeDialog.kt @@ -463,7 +463,10 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( } else items drivesWorking.clear() - val drivesStr = c?.getDrives() ?: Container.DEFAULT_DRIVES + val drivesStr = c?.let { + com.winlator.cmod.runtime.wine.WineUtils.readDrivesFromPrefix(it) + .ifEmpty { it.getDrives() } + } ?: Container.DEFAULT_DRIVES for (drive in Container.drivesIterator(drivesStr)) { drivesWorking.add( DriveDraft( @@ -848,6 +851,10 @@ class ContainerSettingsComposeDialog @JvmOverloads constructor( c.setEmulator64(emulator64) c.setWinComponents(wincomponents) c.setDrives(drivesString) + // an existing prefix owns the drive list, so write it there too + if (java.io.File(c.rootDir, ".wine/dosdevices").isDirectory) { + com.winlator.cmod.runtime.wine.WineUtils.applyDrivesToPrefix(c, drivesString) + } c.setFullscreenStretched(state.fullscreenStretched.value) c.setUseUnixLibs(state.useUnixLibs.value) c.setInputType(finalInputType) diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index b5429ae71..8f0fa1e77 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -6803,7 +6803,8 @@ private void setupXEnvironment() throws PackageManager.NameNotFoundException { normalizeSyncEnvVars(envVars); ArrayList bindingPaths = new ArrayList<>(); - String drives = shortcut != null ? getShortcutSetting("drives", container.getDrives()) : container.getDrives(); + String drives = WineUtils.readDrivesFromPrefix(container); + if (drives.isEmpty()) drives = container.getDrives(); for (String[] drive : Container.drivesIterator(drives)) { bindingPaths.add(drive[1]); } diff --git a/app/src/main/runtime/wine/WineUtils.java b/app/src/main/runtime/wine/WineUtils.java index a64996e82..994336d9c 100644 --- a/app/src/main/runtime/wine/WineUtils.java +++ b/app/src/main/runtime/wine/WineUtils.java @@ -17,6 +17,7 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -60,10 +61,13 @@ public static String hostPathToMappedWinePath( String bestDriveLetter = null; String bestDriveRoot = null; - String drives = - container != null && container.getDrives() != null - ? container.getDrives() - : Container.DEFAULT_DRIVES; + String drives = readDrivesFromPrefix(container); + if (drives.isEmpty()) { + drives = + container != null && container.getDrives() != null + ? container.getDrives() + : Container.DEFAULT_DRIVES; + } for (String[] drive : Container.drivesIterator(drives)) { if (drive.length < 2) continue; @@ -375,50 +379,57 @@ private static boolean isSupportedDriveLetter(String letter) { return letter.length() == 1 && Character.isLetter(letter.charAt(0)); } - public static void createDosdevicesSymlinks( - Container container, @Nullable String gameDirectoryPath, boolean exposeSteamGameLink) { - Log.d( - "ContainerLaunch", - "createDosdevicesSymlinks: rootDir=" - + container.getRootDir().getAbsolutePath() - + " drives=" - + container.getDrives() - + " gameDir=" - + gameDirectoryPath); + // dosdevices is the source of truth for drives; c: and z: stay app-managed + public static String readDrivesFromPrefix(@Nullable Container container) { + if (container == null) return ""; File dosdevicesDir = new File(container.getRootDir(), ".wine/dosdevices"); - if (!dosdevicesDir.exists()) { - boolean created = dosdevicesDir.mkdirs(); - Log.d("ContainerLaunch", "createDosdevicesSymlinks: created dosdevices dir=" + created); - } - String dosdevicesPath = dosdevicesDir.getPath(); File[] files = dosdevicesDir.listFiles(); - if (files != null) for (File file : files) if (file.getName().matches("[a-z]:")) file.delete(); + if (files == null) return ""; + + List names = new ArrayList<>(); + for (File file : files) if (file.getName().matches("[a-z]:")) names.add(file.getName()); + Collections.sort(names); + + StringBuilder drives = new StringBuilder(); + for (String name : names) { + if (name.equals("c:") || name.equals("z:")) continue; + String target = FileUtils.readSymlink(new File(dosdevicesDir, name)); + if (target.isEmpty()) continue; + + String path = target; + if (!new File(target).isAbsolute()) { + File resolved = new File(dosdevicesDir, target); + try { + path = resolved.getCanonicalPath(); + } catch (IOException e) { + path = resolved.getAbsolutePath(); + } + } + if (path.isEmpty() || path.contains(":")) continue; + drives.append(name.substring(0, 1).toUpperCase(Locale.ENGLISH)).append(':').append(path); + } + return drives.toString(); + } - FileUtils.symlink("../drive_c", dosdevicesPath + "/c:"); - FileUtils.symlink(container.getRootDir().getPath() + "/../..", dosdevicesPath + "/z:"); + public static void applyDrivesToPrefix(@Nullable Container container, @Nullable String drives) { + if (container == null) return; + File dosdevicesDir = new File(container.getRootDir(), ".wine/dosdevices"); + if (!dosdevicesDir.exists()) dosdevicesDir.mkdirs(); + String dosdevicesPath = dosdevicesDir.getPath(); String packageStorageSuffix = "/com.winnative.cmod/storage"; String legacyPackageStorageSuffix = "/com.winlator.cmod/storage"; - Context context = null; if (container.getManager() != null && container.getManager().getContext() != null) { - context = container.getManager().getContext(); - String packageName = context.getPackageName(); - packageStorageSuffix = "/" + packageName + "/storage"; + packageStorageSuffix = + "/" + container.getManager().getContext().getPackageName() + "/storage"; } - if (context != null) { - String normalizedDrives = normalizePersistentDrives(context, container.getDrives(), false); - if (normalizedDrives != null - && !normalizedDrives.isEmpty() - && !normalizedDrives.equals(container.getDrives())) { - container.setDrives(normalizedDrives); - Log.d("WineUtils", "Normalized launch drives in memory to: " + normalizedDrives); - } - } + LinkedHashSet applied = new LinkedHashSet<>(); + for (String[] drive : Container.drivesIterator(drives != null ? drives : "")) { + if (drive.length < 2 || "A".equalsIgnoreCase(drive[0])) continue; + String letter = drive[0].toLowerCase(Locale.ENGLISH); + if (letter.equals("c") || letter.equals("z")) continue; - int driveCount = 0; - for (String[] drive : container.drivesIterator()) { - if ("A".equalsIgnoreCase(drive[0])) continue; String path = resolveReadableDrivePath(drive[1]); File linkTarget = new File(path); path = linkTarget.getAbsolutePath(); @@ -428,11 +439,44 @@ public static void createDosdevicesSymlinks( linkTarget.mkdirs(); FileUtils.chmod(linkTarget, 0771); } - FileUtils.symlink(path, dosdevicesPath + "/" + drive[0].toLowerCase(Locale.ENGLISH) + ":"); - Log.d("ContainerLaunch", "createDosdevicesSymlinks: " + drive[0] + ": -> " + path); - driveCount++; + FileUtils.symlink(path, dosdevicesPath + "/" + letter + ":"); + applied.add(letter + ":"); + Log.d("ContainerLaunch", "applyDrivesToPrefix: " + letter + ": -> " + path); } - Log.d("ContainerLaunch", "createDosdevicesSymlinks: created " + driveCount + " drive symlinks"); + + File[] files = dosdevicesDir.listFiles(); + if (files != null) + for (File file : files) { + String name = file.getName(); + if (!name.matches("[a-z]:") || name.equals("c:") || name.equals("z:")) continue; + if (!applied.contains(name)) file.delete(); + } + } + + public static void createDosdevicesSymlinks( + Container container, @Nullable String gameDirectoryPath, boolean exposeSteamGameLink) { + File dosdevicesDir = new File(container.getRootDir(), ".wine/dosdevices"); + if (!dosdevicesDir.exists()) dosdevicesDir.mkdirs(); + String dosdevicesPath = dosdevicesDir.getPath(); + + FileUtils.symlink("../drive_c", dosdevicesPath + "/c:"); + FileUtils.symlink(container.getRootDir().getPath() + "/../..", dosdevicesPath + "/z:"); + + // the container config only seeds a prefix that has no drives yet + String drives = readDrivesFromPrefix(container); + Log.d("ContainerLaunch", "createDosdevicesSymlinks: entries=" + Arrays.toString(dosdevicesDir.list()) + " read=" + drives); + if (drives.isEmpty()) { + drives = container.getDrives(); + Context context = + container.getManager() != null ? container.getManager().getContext() : null; + if (context != null) { + String normalizedDrives = normalizePersistentDrives(context, drives, false); + if (normalizedDrives != null && !normalizedDrives.isEmpty()) drives = normalizedDrives; + } + Log.d("ContainerLaunch", "createDosdevicesSymlinks: seeding fresh prefix with " + drives); + } + + applyDrivesToPrefix(container, drives); // Only expose Steam's steamapps/common symlink for actual Steam launches. if (gameDirectoryPath != null && !gameDirectoryPath.isEmpty()) { From 76749424928fd33b4fb0c9784494f54ad09c3938 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Wed, 29 Jul 2026 16:45:56 +0000 Subject: [PATCH 2/3] Let the directory picker location box accept a typed path Tapping the current-path card opens a text input prefilled with the current directory, focused with the keyboard shown. A directory navigates there, a file path opens its parent with the file selected, anything else reports that the path was not found. The card is registered as a pane-nav item so it is reachable without touch. --- app/src/main/res/values-b+es+419/strings.xml | 2 + app/src/main/res/values-da/strings.xml | 2 + app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-es/strings.xml | 2 + app/src/main/res/values-fi/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-hi/strings.xml | 2 + app/src/main/res/values-it/strings.xml | 2 + app/src/main/res/values-ja/strings.xml | 2 + app/src/main/res/values-ko/strings.xml | 2 + app/src/main/res/values-no/strings.xml | 2 + app/src/main/res/values-pl/strings.xml | 2 + app/src/main/res/values-pt-rBR/strings.xml | 2 + app/src/main/res/values-pt/strings.xml | 2 + app/src/main/res/values-ro/strings.xml | 2 + app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-sv/strings.xml | 2 + app/src/main/res/values-th/strings.xml | 2 + app/src/main/res/values-tr/strings.xml | 2 + app/src/main/res/values-uk/strings.xml | 2 + app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values-zh-rTW/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../shared/android/DirectoryPickerDialog.kt | 55 +++++++++++++++++-- 24 files changed, 97 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 9919ec9cb..899617cc9 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -15,6 +15,8 @@ Seleccionar EXE Seleccionar carpeta Carpeta actual + Ingresar ruta + Ruta no encontrada Selecciona un archivo .exe válido Selecciona un archivo .ico o .png válido. Salir diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 7c2bc7ae8..0afcb6890 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -128,6 +128,8 @@ Vælg Vælg mappe Nuværende mappe + Indtast sti + Stien blev ikke fundet Lagringsrødder Gennemse lokale mapper direkte Ingen mapper tilgængelige her diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 303dbfa8e..ffc75f99e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -128,6 +128,8 @@ Auswählen Ordner auswählen Aktueller Ordner + Pfad eingeben + Pfad nicht gefunden Speicherorte Lokale Ordner direkt durchsuchen Hier sind keine Ordner verfügbar diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5cc622090..40ce3eb87 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -128,6 +128,8 @@ Seleccionar Seleccionar carpeta Carpeta actual + Introducir ruta + Ruta no encontrada Ubicaciones de almacenamiento Explorar carpetas locales directamente No hay carpetas disponibles aquí diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index f62d0a67c..cc948e9f1 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -15,6 +15,8 @@ Valitse EXE Valitse kansio Nykyinen kansio + Anna polku + Polkua ei löytynyt Valitse kelvollinen .exe-tiedosto Valitse kelvollinen .ico- tai .png-tiedosto. Poistu diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e8865ddbc..cc6348beb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -128,6 +128,8 @@ Sélectionner Sélectionner un dossier Dossier actuel + Saisir le chemin + Chemin introuvable Emplacements de stockage Parcourir directement les dossiers locaux Aucun dossier disponible ici diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 63edff4cd..1c318e812 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -15,6 +15,8 @@ EXE चुनें फ़ोल्डर चुनें वर्तमान फ़ोल्डर + पथ दर्ज करें + पथ नहीं मिला कृपया मान्य .exe फ़ाइल चुनें कृपया मान्य .ico या .png फ़ाइल चुनें। बाहर निकलें diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 99e2cf56c..aa3d9ce0b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -128,6 +128,8 @@ Seleziona Seleziona cartella Cartella corrente + Inserisci percorso + Percorso non trovato Percorsi di archiviazione Sfoglia direttamente le cartelle locali Nessuna cartella disponibile qui diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f444592f9..f1dede4f0 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -15,6 +15,8 @@ EXEを選択 フォルダを選択 現在のフォルダ + パスを入力 + パスが見つかりません 有効な.exeファイルを選択してください 有効な.icoまたは.pngファイルを選択してください。 終了 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3d9dd0d1c..65706aba0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -128,6 +128,8 @@ 선택 폴더 선택 현재 폴더 + 경로 입력 + 경로를 찾을 수 없음 저장소 루트 로컬 폴더를 바로 탐색 여기에서 사용할 수 있는 폴더가 없습니다 diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 144ab9a8e..e6ee7acf6 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -15,6 +15,8 @@ Velg EXE Velg mappe Gjeldende mappe + Angi sti + Fant ikke stien Velg en gyldig .exe-fil Velg en gyldig .ico- eller .png-fil. Avslutt diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 33f7e49df..402e17c95 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -128,6 +128,8 @@ Wybierz Wybierz folder Bieżący folder + Wprowadź ścieżkę + Nie znaleziono ścieżki Lokalizacje pamięci Przeglądaj lokalne foldery bezpośrednio Brak dostępnych folderów w tej lokalizacji diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 144328d65..8dc46b6ae 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -128,6 +128,8 @@ Selecionar Selecionar pasta Pasta atual + Inserir caminho + Caminho não encontrado Locais de armazenamento Navegar pelas pastas locais diretamente Nenhuma pasta disponível aqui diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 36753863d..ed79dea01 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -15,6 +15,8 @@ Selecionar EXE Selecionar Pasta Pasta Atual + Introduzir caminho + Caminho não encontrado Selecione um ficheiro .exe válido Selecione um ficheiro .ico ou .png válido. Sair diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 2577a0474..a4a1f22cc 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -128,6 +128,8 @@ Selectează Selectează folder Folder curent + Introduceți calea + Calea nu a fost găsită Locații de stocare Răsfoiește direct folderele locale Niciun folder disponibil aici diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d63098f20..b11a7a883 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,6 +15,8 @@ Выбрать EXE Выбрать папку Текущая папка + Введите путь + Путь не найден Выберите корректный файл .exe Выберите корректный файл .ico или .png. Выход diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 5a9dfd660..e57383056 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -15,6 +15,8 @@ Välj EXE Välj mapp Aktuell mapp + Ange sökväg + Sökvägen hittades inte Välj en giltig .exe-fil Välj en giltig .ico- eller .png-fil. Avsluta diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 048f4e759..19e33d9f4 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -15,6 +15,8 @@ เลือก EXE เลือกโฟลเดอร์ โฟลเดอร์ปัจจุบัน + ป้อนพาธ + ไม่พบพาธ โปรดเลือกไฟล์ .exe ที่ถูกต้อง โปรดเลือกไฟล์ .ico หรือ .png ที่ถูกต้อง ออก diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index acd1fe4f0..9d3407a01 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -15,6 +15,8 @@ EXE Seç Klasör Seç Geçerli Klasör + Yol girin + Yol bulunamadı Lütfen geçerli bir .exe dosyası seçin Lütfen geçerli bir .ico veya .png dosyası seçin. Çıkış diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index aa2251751..b6dcc341c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -128,6 +128,8 @@ Вибрати Вибрати теку Поточна тека + Введіть шлях + Шлях не знайдено Сховища Переглядати локальні теки безпосередньо Тут немає доступних тек diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fb359c30b..59d94be2b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -128,6 +128,8 @@ 选择 选择文件夹 当前文件夹 + 输入路径 + 找不到路径 存储根目录 直接浏览本地文件夹 此处没有可用的文件夹 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 6312633aa..6b75c25c0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -128,6 +128,8 @@ 選擇 選擇資料夾 目前資料夾 + 輸入路徑 + 找不到路徑 儲存根目錄 直接瀏覽本機資料夾 這裡沒有可用的資料夾 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 364f9c208..aa83bcf14 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,8 @@ Select EXE Select Folder Current Folder + Enter Path + Path not found Please select a valid .exe file Please select a valid .ico or .png file. Exit diff --git a/app/src/main/shared/android/DirectoryPickerDialog.kt b/app/src/main/shared/android/DirectoryPickerDialog.kt index 25986ac6f..4e1f66ce9 100644 --- a/app/src/main/shared/android/DirectoryPickerDialog.kt +++ b/app/src/main/shared/android/DirectoryPickerDialog.kt @@ -75,6 +75,9 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.OutlinedTextField +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.material3.ripple import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -419,6 +422,7 @@ object DirectoryPickerDialog { val scope = rememberCoroutineScope() var currentDir by remember(initialDir.absolutePath) { mutableStateOf(initialDir) } var selectedFile by remember { mutableStateOf(null) } + var showPathInput by remember { mutableStateOf(false) } var rootsExpanded by remember { mutableStateOf(false) } var refreshTick by remember { mutableStateOf(0) } var clipboard by remember { mutableStateOf(null) } @@ -898,7 +902,10 @@ object DirectoryPickerDialog { } Spacer(Modifier.height(4.dp)) - CurrentPathCard(path = selectedFile?.name ?: currentDir.absolutePath) + CurrentPathCard( + path = selectedFile?.name ?: currentDir.absolutePath, + onClick = { showPathInput = true }, + ) Spacer(Modifier.height(8.dp)) androidx.compose.foundation.layout.Box( @@ -1186,6 +1193,32 @@ object DirectoryPickerDialog { } } } + + if (showPathInput) { + val pathNotFound = activityString(R.string.common_ui_path_not_found) + CompositionLocalProvider(LocalPaneNav provides overlayRegistry) { + TextInputOverlay( + modifier = Modifier.matchParentSize(), + title = activityString(R.string.common_ui_enter_path), + initial = currentDir.absolutePath, + confirmLabel = activityString(R.string.common_ui_ok), + onConfirm = { typed -> + val target = File(typed.trim()) + if (target.isDirectory) { + currentDir = target + selectedFile = null + } else if (target.isFile) { + currentDir = target.parentFile ?: currentDir + selectedFile = target + } else { + Toast.makeText(context, pathNotFound, Toast.LENGTH_SHORT).show() + } + showPathInput = false + }, + onDismiss = { showPathInput = false }, + ) + } + } } } @@ -1236,15 +1269,23 @@ object DirectoryPickerDialog { } @Composable - private fun CurrentPathCard(path: String) { + private fun CurrentPathCard( + path: String, + onClick: () -> Unit, + ) { Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) + .paneNavItem(cornerRadius = 12.dp, onActivate = onClick) .background(BgDark) .border(1.dp, CardBorder, RoundedCornerShape(12.dp)) - .padding(horizontal = CurrentPathHorizontalPadding, vertical = 4.dp), + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ).padding(horizontal = CurrentPathHorizontalPadding, vertical = 4.dp), contentAlignment = Alignment.CenterStart, ) { Text( @@ -2015,6 +2056,12 @@ object DirectoryPickerDialog { onDismiss: () -> Unit, ) { var text by remember { mutableStateOf(initial) } + val focusRequester = remember { FocusRequester() } + val keyboard = LocalSoftwareKeyboardController.current + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboard?.show() + } Box( modifier = modifier @@ -2050,7 +2097,7 @@ object DirectoryPickerDialog { value = text, onValueChange = { text = it }, singleLine = true, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), ) Spacer(Modifier.height(16.dp)) Row( From a682083482540e92f6ade5e167e4db0a742acb33 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Wed, 29 Jul 2026 22:57:31 +0000 Subject: [PATCH 3/3] Route pane-nav to the path popup and let Start confirm it The path input was only wrapped in the overlay registry, so d-pad keys still resolved to the grid registry and moved the directory selection behind it, leaving OK and Cancel unreachable. Its visibility now feeds the registry selector, the Start guard, the dismiss chain and the overlay reset. Start confirms the typed path instead of acting on the picker underneath, sharing one apply path with the OK button. --- .../shared/android/DirectoryPickerDialog.kt | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/app/src/main/shared/android/DirectoryPickerDialog.kt b/app/src/main/shared/android/DirectoryPickerDialog.kt index 4e1f66ce9..6e41f99f2 100644 --- a/app/src/main/shared/android/DirectoryPickerDialog.kt +++ b/app/src/main/shared/android/DirectoryPickerDialog.kt @@ -423,6 +423,7 @@ object DirectoryPickerDialog { var currentDir by remember(initialDir.absolutePath) { mutableStateOf(initialDir) } var selectedFile by remember { mutableStateOf(null) } var showPathInput by remember { mutableStateOf(false) } + var pathInputText by remember { mutableStateOf("") } var rootsExpanded by remember { mutableStateOf(false) } var refreshTick by remember { mutableStateOf(0) } var clipboard by remember { mutableStateOf(null) } @@ -735,7 +736,7 @@ object DirectoryPickerDialog { footerZone = false } LaunchedEffect(menuTarget) { if (menuTarget != null) menuRegistry.reset() } - LaunchedEffect(renameTarget, showNewFolder, deleteTargets, runTarget, transferProgress != null) { + LaunchedEffect(renameTarget, showNewFolder, showPathInput, deleteTargets, runTarget, transferProgress != null) { overlayRegistry.controllerActive = false overlayRegistry.reset() } @@ -757,11 +758,28 @@ object DirectoryPickerDialog { footerRegistry.controllerActive = false contentRegistry.controllerActive = true } + val pathNotFound = activityString(R.string.common_ui_path_not_found) + + fun applyTypedPath(typed: String) { + val target = File(typed.trim()) + if (target.isDirectory) { + currentDir = target + selectedFile = null + } else if (target.isFile) { + currentDir = target.parentFile ?: currentDir + selectedFile = target + } else { + Toast.makeText(context, pathNotFound, Toast.LENGTH_SHORT).show() + } + showPathInput = false + } + val handlers = remember(window) { paneNavHandlers( onDismiss = { when { + showPathInput -> showPathInput = false renameTarget != null -> renameTarget = null showNewFolder -> showNewFolder = false deleteTargets != null -> deleteTargets = null @@ -779,9 +797,11 @@ object DirectoryPickerDialog { onStart = { val overlayOpen = rootsExpanded || menuTarget != null || renameTarget != null || - showNewFolder || deleteTargets != null || runTarget != null || - transferProgress != null - if (!overlayOpen) { + showNewFolder || showPathInput || deleteTargets != null || + runTarget != null || transferProgress != null + if (showPathInput) { + applyTypedPath(pathInputText) + } else if (!overlayOpen) { if (manage) { if (clipboard != null) pasteInto(currentDir) else onDismiss() } else { @@ -795,8 +815,9 @@ object DirectoryPickerDialog { when { menuTarget != null -> menuRegistry rootsExpanded -> rootsRegistry - renameTarget != null || showNewFolder || deleteTargets != null || - runTarget != null || transferProgress != null -> overlayRegistry + renameTarget != null || showNewFolder || showPathInput || + deleteTargets != null || runTarget != null || + transferProgress != null -> overlayRegistry footerZone -> footerRegistry else -> contentRegistry } @@ -904,7 +925,10 @@ object DirectoryPickerDialog { Spacer(Modifier.height(4.dp)) CurrentPathCard( path = selectedFile?.name ?: currentDir.absolutePath, - onClick = { showPathInput = true }, + onClick = { + pathInputText = currentDir.absolutePath + showPathInput = true + }, ) Spacer(Modifier.height(8.dp)) @@ -1195,26 +1219,14 @@ object DirectoryPickerDialog { } if (showPathInput) { - val pathNotFound = activityString(R.string.common_ui_path_not_found) CompositionLocalProvider(LocalPaneNav provides overlayRegistry) { TextInputOverlay( modifier = Modifier.matchParentSize(), title = activityString(R.string.common_ui_enter_path), - initial = currentDir.absolutePath, + initial = pathInputText, confirmLabel = activityString(R.string.common_ui_ok), - onConfirm = { typed -> - val target = File(typed.trim()) - if (target.isDirectory) { - currentDir = target - selectedFile = null - } else if (target.isFile) { - currentDir = target.parentFile ?: currentDir - selectedFile = target - } else { - Toast.makeText(context, pathNotFound, Toast.LENGTH_SHORT).show() - } - showPathInput = false - }, + onTextChange = { pathInputText = it }, + onConfirm = { applyTypedPath(it) }, onDismiss = { showPathInput = false }, ) } @@ -2054,6 +2066,7 @@ object DirectoryPickerDialog { confirmLabel: String, onConfirm: (String) -> Unit, onDismiss: () -> Unit, + onTextChange: (String) -> Unit = {}, ) { var text by remember { mutableStateOf(initial) } val focusRequester = remember { FocusRequester() } @@ -2095,7 +2108,10 @@ object DirectoryPickerDialog { Spacer(Modifier.height(12.dp)) OutlinedTextField( value = text, - onValueChange = { text = it }, + onValueChange = { + text = it + onTextChange(it) + }, singleLine = true, modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), )