diff --git a/src-tauri/nsis/installer.nsi b/src-tauri/nsis/installer.nsi index 5f72bae..b8dab09 100644 --- a/src-tauri/nsis/installer.nsi +++ b/src-tauri/nsis/installer.nsi @@ -587,11 +587,18 @@ Function .onInit ; --- PORTABLE MODE --- Auto-detect portable mode during updates. - ; If the target directory already has a portable marker file, preserve + ; If the target directory already has a valid portable marker file, preserve ; portable mode so the Tauri updater works without needing /PORTABLE. + ; We validate the magic string to avoid false-positives from stale empty files + ; left by scoop's NSIS extraction (dl.7z side-effect). ${If} $PortableMode <> 1 ${AndIf} ${FileExists} "$INSTDIR\portable" - StrCpy $PortableMode 1 + FileOpen $1 "$INSTDIR\portable" r + FileRead $1 $2 + FileClose $1 + ${If} $2 == "Handy Portable Mode" + StrCpy $PortableMode 1 + ${EndIf} ${EndIf} !if "${INSTALLMODE}" == "both" @@ -757,6 +764,7 @@ Section Install ; --- PORTABLE MODE --- Create portable marker and Data directory ${If} $PortableMode = 1 FileOpen $0 "$INSTDIR\portable" w + FileWrite $0 "Handy Portable Mode" FileClose $0 CreateDirectory "$INSTDIR\Data" DetailPrint "Portable mode: created marker file and Data directory." @@ -1050,11 +1058,6 @@ Function CreateOrUpdateStartMenuShortcut FunctionEnd Function CreateOrUpdateDesktopShortcut - ; --- PORTABLE MODE --- No desktop shortcut for portable installs - ${If} $PortableMode = 1 - Return - ${EndIf} - ; We used to use product name as MAINBINARYNAME ; migrate old shortcuts to target the new MAINBINARYNAME !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName" diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 415efb0..45e4fdc 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -14,6 +14,12 @@ pub fn cancel_operation(app: AppHandle) { cancel_current_operation(&app); } +#[tauri::command] +#[specta::specta] +pub fn is_portable() -> bool { + crate::portable::is_portable() +} + #[tauri::command] #[specta::specta] pub fn get_app_dir_path(app: AppHandle) -> Result { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 45dd1df..5d1ca10 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -375,6 +375,7 @@ pub fn run(cli_args: CliArgs) { trigger_update_check, show_main_window_command, commands::cancel_operation, + commands::is_portable, commands::get_app_dir_path, commands::get_app_settings, commands::get_default_settings, diff --git a/src-tauri/src/portable.rs b/src-tauri/src/portable.rs index 8d79433..5d7bf7d 100644 --- a/src-tauri/src/portable.rs +++ b/src-tauri/src/portable.rs @@ -17,8 +17,23 @@ pub fn init() { let exe_path = std::env::current_exe().ok()?; let exe_dir = exe_path.parent()?; - if exe_dir.join("portable").exists() { - let data_dir = exe_dir.join("Data"); + let marker_path = exe_dir.join("portable"); + let data_dir = exe_dir.join("Data"); + + let is_portable = if is_valid_portable_marker(&marker_path) { + true + } else if marker_path.exists() && data_dir.exists() { + // Migration: v0.8.0 created an empty marker file. If we find an + // empty/invalid marker alongside an existing Data/ dir, this is a + // real portable install — upgrade the marker in place. + eprintln!("[portable] upgrading legacy empty marker to magic string"); + let _ = std::fs::write(&marker_path, "Handy Portable Mode"); + true + } else { + false + }; + + if is_portable { if !data_dir.exists() { std::fs::create_dir_all(&data_dir).ok()?; } @@ -75,3 +90,77 @@ pub fn store_path(relative: &str) -> PathBuf { PathBuf::from(relative) } } + +/// Check if a marker file path contains the portable magic string. +/// Extracted for testability. +fn is_valid_portable_marker(path: &std::path::Path) -> bool { + std::fs::read_to_string(path) + .map(|s| s.trim().starts_with("Handy Portable Mode")) + .unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn test_valid_magic_string_enables_portable() { + let dir = std::env::temp_dir().join("handy_test_valid"); + std::fs::create_dir_all(&dir).unwrap(); + let marker = dir.join("portable"); + let mut f = std::fs::File::create(&marker).unwrap(); + write!(f, "Handy Portable Mode").unwrap(); + assert!(is_valid_portable_marker(&marker)); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn test_empty_file_does_not_enable_portable() { + let dir = std::env::temp_dir().join("handy_test_empty"); + std::fs::create_dir_all(&dir).unwrap(); + let marker = dir.join("portable"); + std::fs::File::create(&marker).unwrap(); + assert!(!is_valid_portable_marker(&marker)); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn test_wrong_content_does_not_enable_portable() { + let dir = std::env::temp_dir().join("handy_test_wrong"); + std::fs::create_dir_all(&dir).unwrap(); + let marker = dir.join("portable"); + let mut f = std::fs::File::create(&marker).unwrap(); + write!(f, "some other content").unwrap(); + assert!(!is_valid_portable_marker(&marker)); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn test_missing_file_does_not_enable_portable() { + let path = std::path::Path::new("/nonexistent/portable"); + assert!(!is_valid_portable_marker(path)); + } + + #[test] + fn test_legacy_empty_marker_without_data_dir_does_not_enable_portable() { + // Empty marker alone (scoop scenario) — no Data/ dir → not portable + let dir = std::env::temp_dir().join("handy_test_legacy_no_data"); + std::fs::create_dir_all(&dir).unwrap(); + let marker = dir.join("portable"); + std::fs::File::create(&marker).unwrap(); + assert!(!is_valid_portable_marker(&marker)); + std::fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn test_magic_string_with_whitespace_enables_portable() { + let dir = std::env::temp_dir().join("handy_test_ws"); + std::fs::create_dir_all(&dir).unwrap(); + let marker = dir.join("portable"); + let mut f = std::fs::File::create(&marker).unwrap(); + write!(f, " Handy Portable Mode\n").unwrap(); + assert!(is_valid_portable_marker(&marker)); + std::fs::remove_dir_all(dir).unwrap(); + } +} diff --git a/src/bindings.ts b/src/bindings.ts index 260cf9f..c405954 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -426,6 +426,9 @@ async showMainWindowCommand() : Promise> { async cancelOperation() : Promise { await TAURI_INVOKE("cancel_operation"); }, +async isPortable() : Promise { + return await TAURI_INVOKE("is_portable"); +}, async getAppDirPath() : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_app_dir_path") }; diff --git a/src/components/update-checker/UpdateChecker.tsx b/src/components/update-checker/UpdateChecker.tsx index 44bd206..4137dd7 100644 --- a/src/components/update-checker/UpdateChecker.tsx +++ b/src/components/update-checker/UpdateChecker.tsx @@ -3,8 +3,10 @@ import { useTranslation } from "react-i18next"; import { check } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; import { listen } from "@tauri-apps/api/event"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { ProgressBar } from "../shared"; import { useSettings } from "../../hooks/useSettings"; +import { commands } from "../../bindings"; interface UpdateCheckerProps { className?: string; @@ -18,6 +20,8 @@ const UpdateChecker: React.FC = ({ className = "" }) => { const [isInstalling, setIsInstalling] = useState(false); const [downloadProgress, setDownloadProgress] = useState(0); const [showUpToDate, setShowUpToDate] = useState(false); + const [showPortableUpdateDialog, setShowPortableUpdateDialog] = + useState(false); const { settings, isLoading } = useSettings(); const settingsLoaded = !isLoading && settings !== null; @@ -97,6 +101,13 @@ const UpdateChecker: React.FC = ({ className = "" }) => { const installUpdate = async () => { if (!updateChecksEnabled) return; + + const portable = await commands.isPortable(); + if (portable) { + setShowPortableUpdateDialog(true); + return; + } + try { setIsInstalling(true); setDownloadProgress(0); @@ -172,37 +183,68 @@ const UpdateChecker: React.FC = ({ className = "" }) => { !isUpdateDisabled && (updateAvailable || (!isChecking && !showUpToDate)); return ( -
- {isUpdateClickable ? ( - - ) : ( - - {getUpdateStatusText()} - + <> + {showPortableUpdateDialog && ( +
+
+

+ {t("footer.portableUpdateTitle")} +

+

+ {t("footer.portableUpdateMessage")} +

+
+ + +
+
+
)} +
+ {isUpdateClickable ? ( + + ) : ( + + {getUpdateStatusText()} + + )} - {isInstalling && downloadProgress > 0 && downloadProgress < 100 && ( - - )} -
+ {isInstalling && downloadProgress > 0 && downloadProgress < 100 && ( + + )} +
+ ); }; diff --git a/src/i18n/locales/ar/translation.json b/src/i18n/locales/ar/translation.json index 55ce0fe..910884a 100644 --- a/src/i18n/locales/ar/translation.json +++ b/src/i18n/locales/ar/translation.json @@ -542,7 +542,10 @@ "downloading": "...جاري التنزيل {{progress}}%", "installing": "...جاري التثبيت", "preparing": "...جاري التحضير", - "checkForUpdates": "التحقق من وجود تحديثات" + "checkForUpdates": "التحقق من وجود تحديثات", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "...جاري التحميل", diff --git a/src/i18n/locales/cs/translation.json b/src/i18n/locales/cs/translation.json index 0dd34f3..f4e6b0c 100644 --- a/src/i18n/locales/cs/translation.json +++ b/src/i18n/locales/cs/translation.json @@ -542,7 +542,10 @@ "downloading": "Stahování... {{progress}}%", "installing": "Instalace...", "preparing": "Příprava...", - "checkForUpdates": "Zkontrolovat aktualizace" + "checkForUpdates": "Zkontrolovat aktualizace", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Načítání...", diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 249c274..30732c3 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -542,7 +542,10 @@ "downloading": "Wird heruntergeladen... {{progress}}%", "installing": "Wird installiert...", "preparing": "Wird vorbereitet...", - "checkForUpdates": "Nach Updates suchen" + "checkForUpdates": "Nach Updates suchen", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Wird geladen...", diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 583bdbb..c0cf114 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -542,7 +542,10 @@ "downloading": "Downloading... {{progress}}%", "installing": "Installing...", "preparing": "Preparing...", - "checkForUpdates": "Check for updates" + "checkForUpdates": "Check for updates", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Loading...", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index dcfdef8..9f79927 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -542,7 +542,10 @@ "downloading": "Descargando... {{progress}}%", "installing": "Instalando...", "preparing": "Preparando...", - "checkForUpdates": "Buscar actualizaciones" + "checkForUpdates": "Buscar actualizaciones", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Cargando...", diff --git a/src/i18n/locales/fr/translation.json b/src/i18n/locales/fr/translation.json index 7953c44..6f98550 100644 --- a/src/i18n/locales/fr/translation.json +++ b/src/i18n/locales/fr/translation.json @@ -542,7 +542,10 @@ "downloading": "Téléchargement... {{progress}}%", "installing": "Installation...", "preparing": "Préparation...", - "checkForUpdates": "Rechercher des mises à jour" + "checkForUpdates": "Rechercher des mises à jour", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Chargement...", diff --git a/src/i18n/locales/it/translation.json b/src/i18n/locales/it/translation.json index 259d0b3..3b724f1 100644 --- a/src/i18n/locales/it/translation.json +++ b/src/i18n/locales/it/translation.json @@ -542,7 +542,10 @@ "downloading": "Download... {{progress}}%", "installing": "Installazione...", "preparing": "Preparazione...", - "checkForUpdates": "Controlla aggiornamenti" + "checkForUpdates": "Controlla aggiornamenti", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Caricamento...", diff --git a/src/i18n/locales/ja/translation.json b/src/i18n/locales/ja/translation.json index 815baee..b0d6ded 100644 --- a/src/i18n/locales/ja/translation.json +++ b/src/i18n/locales/ja/translation.json @@ -542,7 +542,10 @@ "downloading": "ダウンロード中... {{progress}}%", "installing": "インストール中...", "preparing": "準備中...", - "checkForUpdates": "アップデートを確認" + "checkForUpdates": "アップデートを確認", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "読み込み中...", diff --git a/src/i18n/locales/ko/translation.json b/src/i18n/locales/ko/translation.json index e76569e..fe2219e 100644 --- a/src/i18n/locales/ko/translation.json +++ b/src/i18n/locales/ko/translation.json @@ -542,7 +542,10 @@ "downloading": "다운로드 중... {{progress}}%", "installing": "설치 중...", "preparing": "준비 중...", - "checkForUpdates": "업데이트 확인" + "checkForUpdates": "업데이트 확인", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "로딩 중...", diff --git a/src/i18n/locales/pl/translation.json b/src/i18n/locales/pl/translation.json index 7b6ae7c..48067a1 100644 --- a/src/i18n/locales/pl/translation.json +++ b/src/i18n/locales/pl/translation.json @@ -542,7 +542,10 @@ "downloading": "Pobieranie... {{progress}}%", "installing": "Instalowanie...", "preparing": "Przygotowywanie...", - "checkForUpdates": "Sprawdź aktualizacje" + "checkForUpdates": "Sprawdź aktualizacje", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Wczytywanie...", diff --git a/src/i18n/locales/pt/translation.json b/src/i18n/locales/pt/translation.json index c092dfe..eee81c9 100644 --- a/src/i18n/locales/pt/translation.json +++ b/src/i18n/locales/pt/translation.json @@ -542,7 +542,10 @@ "downloading": "Baixando... {{progress}}%", "installing": "Instalando...", "preparing": "Preparando...", - "checkForUpdates": "Verificar atualizações" + "checkForUpdates": "Verificar atualizações", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Carregando...", diff --git a/src/i18n/locales/ru/translation.json b/src/i18n/locales/ru/translation.json index cdfc56a..bb12c5e 100644 --- a/src/i18n/locales/ru/translation.json +++ b/src/i18n/locales/ru/translation.json @@ -542,7 +542,10 @@ "downloading": "Загрузка... {{progress}} %", "installing": "Установка...", "preparing": "Подготовка...", - "checkForUpdates": "Проверить наличие обновлений" + "checkForUpdates": "Проверить наличие обновлений", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Загрузка...", diff --git a/src/i18n/locales/sv/translation.json b/src/i18n/locales/sv/translation.json index d865607..16f33b5 100644 --- a/src/i18n/locales/sv/translation.json +++ b/src/i18n/locales/sv/translation.json @@ -542,7 +542,10 @@ "downloading": "Laddar ner... {{progress}}%", "installing": "Installerar...", "preparing": "Förbereder...", - "checkForUpdates": "Sök efter uppdateringar" + "checkForUpdates": "Sök efter uppdateringar", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Laddar...", diff --git a/src/i18n/locales/tr/translation.json b/src/i18n/locales/tr/translation.json index a005f35..db2982c 100644 --- a/src/i18n/locales/tr/translation.json +++ b/src/i18n/locales/tr/translation.json @@ -542,7 +542,10 @@ "downloading": "İndiriliyor... %{{progress}}", "installing": "Yükleniyor...", "preparing": "Hazırlanıyor...", - "checkForUpdates": "Güncellemeleri kontrol et" + "checkForUpdates": "Güncellemeleri kontrol et", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Yükleniyor...", diff --git a/src/i18n/locales/uk/translation.json b/src/i18n/locales/uk/translation.json index e031f53..6902c17 100644 --- a/src/i18n/locales/uk/translation.json +++ b/src/i18n/locales/uk/translation.json @@ -542,7 +542,10 @@ "downloading": "Завантаження... {{progress}}%", "installing": "Встановлення...", "preparing": "Підготовка...", - "checkForUpdates": "Перевірити оновлення" + "checkForUpdates": "Перевірити оновлення", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Завантаження...", diff --git a/src/i18n/locales/vi/translation.json b/src/i18n/locales/vi/translation.json index bacbb7b..aaaa64c 100644 --- a/src/i18n/locales/vi/translation.json +++ b/src/i18n/locales/vi/translation.json @@ -542,7 +542,10 @@ "downloading": "Đang tải... {{progress}}%", "installing": "Đang cài đặt...", "preparing": "Đang chuẩn bị...", - "checkForUpdates": "Kiểm tra cập nhật" + "checkForUpdates": "Kiểm tra cập nhật", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "Đang tải...", diff --git a/src/i18n/locales/zh-TW/translation.json b/src/i18n/locales/zh-TW/translation.json index f5c1421..6c2ad52 100644 --- a/src/i18n/locales/zh-TW/translation.json +++ b/src/i18n/locales/zh-TW/translation.json @@ -542,7 +542,10 @@ "downloading": "下載中... {{progress}}%", "installing": "安裝中...", "preparing": "準備中...", - "checkForUpdates": "檢查更新" + "checkForUpdates": "檢查更新", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "載入中...", diff --git a/src/i18n/locales/zh/translation.json b/src/i18n/locales/zh/translation.json index 3958e23..34605b5 100644 --- a/src/i18n/locales/zh/translation.json +++ b/src/i18n/locales/zh/translation.json @@ -542,7 +542,10 @@ "downloading": "下载中... {{progress}}%", "installing": "安装中...", "preparing": "准备中...", - "checkForUpdates": "检查更新" + "checkForUpdates": "检查更新", + "portableUpdateTitle": "Manual update required", + "portableUpdateMessage": "Portable installs cannot be updated automatically. To update: download the latest NSIS installer from GitHub Releases, install it to the same folder, then copy your Data/ folder (settings, models, recordings) from the old version to the new one.", + "portableUpdateButton": "Open GitHub Releases" }, "common": { "loading": "加载中...",