From 5e68f7a73f39012340665047ca666ccfed25353f Mon Sep 17 00:00:00 2001 From: Ain Date: Sat, 10 Oct 2026 18:47:53 +0800 Subject: [PATCH 1/3] feat(i18n): add Traditional Chinese (zh-TW) localization --- gui/lib/l10n/app_localizations.dart | 9 +- gui/lib/l10n/app_localizations_zh_TW.dart | 475 ++++++++++++++++++++++ gui/lib/l10n/app_zh_TW.arb | 228 +++++++++++ gui/lib/providers/locale_provider.dart | 28 +- 4 files changed, 737 insertions(+), 3 deletions(-) create mode 100644 gui/lib/l10n/app_localizations_zh_TW.dart create mode 100644 gui/lib/l10n/app_zh_TW.arb diff --git a/gui/lib/l10n/app_localizations.dart b/gui/lib/l10n/app_localizations.dart index 5861ff8..82243de 100644 --- a/gui/lib/l10n/app_localizations.dart +++ b/gui/lib/l10n/app_localizations.dart @@ -16,6 +16,7 @@ import 'app_localizations_pt.dart'; import 'app_localizations_ru.dart'; import 'app_localizations_tr.dart'; import 'app_localizations_zh.dart'; +import 'app_localizations_zh_TW.dart'; // ignore_for_file: type=lint @@ -113,7 +114,8 @@ abstract class AppLocalizations { Locale('pt'), Locale('ru'), Locale('tr'), - Locale('zh') + Locale('zh'), + Locale('zh_TW') ]; /// No description provided for @appTitle. @@ -1039,7 +1041,12 @@ AppLocalizations lookupAppLocalizations(Locale locale) { case 'tr': return AppLocalizationsTr(); case 'zh': + if (locale.countryCode == 'TW' || locale.countryCode == 'HK' || locale.countryCode == 'MO') { + return AppLocalizationsZhTw(); + } return AppLocalizationsZh(); + case 'zh_TW': + return AppLocalizationsZhTw(); } throw FlutterError( diff --git a/gui/lib/l10n/app_localizations_zh_TW.dart b/gui/lib/l10n/app_localizations_zh_TW.dart new file mode 100644 index 0000000..a114fe9 --- /dev/null +++ b/gui/lib/l10n/app_localizations_zh_TW.dart @@ -0,0 +1,475 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese, as used in Taiwan (`zh_TW`). +class AppLocalizationsZhTw extends AppLocalizations { + AppLocalizationsZhTw([String locale = 'zh_TW']) : super(locale); + + @override + String get appTitle => 'OpenROM'; + + @override + String get appSubtitle => '萬用 ROM 轉換工具'; + + @override + String get convertButton => '轉換'; + + @override + String convertButtonWithCount(int count) { + return '轉換($count 個檔案)'; + } + + @override + String get dropZoneHint => '將 ROM 檔案拖曳至此'; + + @override + String get dropZoneSubHint => '或點擊瀏覽'; + + @override + String get settingsTitle => '轉換設定'; + + @override + String get outputFormat => '輸出格式'; + + @override + String get compressionLevel => '壓縮層級'; + + @override + String get compressionNormal => '標準'; + + @override + String get compressionHigh => '高'; + + @override + String get compressionMax => '最高'; + + @override + String get postProcessing => '後續處理'; + + @override + String get verifyAfterConversion => '轉換後驗證'; + + @override + String get outputDestination => '輸出目標'; + + @override + String get browse => '瀏覽'; + + @override + String get statusWaiting => '等待中...'; + + @override + String get statusConverting => '轉換中...'; + + @override + String get statusDone => '完成'; + + @override + String get statusFailed => '失敗'; + + @override + String get aboutTitle => '關於 OpenROM'; + + @override + String get settingsScreen => '設定'; + + @override + String get aboutScreen => '關於'; + + @override + String get themeScreen => '主題'; + + @override + String get errorCoreNotFound => + '應用程式資料夾中缺少 openrom-core。'; + + @override + String get errorUnsupportedFormat => + '此檔案格式無法使用選擇的輸出格式進行轉換。'; + + @override + String get errorDiskSpace => + '可用空間不足以完成此次轉換。'; + + @override + String get errorToolFailed => + '轉換工具(chdman/maxcso)未預期結束。'; + + @override + String get errorConversionFailed => '轉換在完成前停止。'; + + @override + String get errorFileNotFound => '輸入檔案已被移動或刪除。'; + + @override + String get errorOutputDirNotFound => + '選擇的輸出資料夾已不存在。'; + + @override + String get errorPermissionDenied => + 'OpenROM 無法寫入輸出資料夾。'; + + @override + String get errorCorruptedFile => + '檔案似乎已損毀或為 0 bytes。'; + + @override + String get errorUnknown => '發生錯誤。這可能是程式缺陷。'; + + @override + String get errorDetailsLabel => '詳細資料'; + + @override + String get retryButton => '重試'; + + @override + String get closeButton => '關閉'; + + @override + String get reportBugButton => '回報問題'; + + @override + String get clearQueue => '清除佇列'; + + @override + String get removeFile => '移除'; + + @override + String get openOutputFolder => '開啟輸出資料夾'; + + @override + String get patcherTitle => 'ROM 修補器'; + + @override + String get patcherRomFile => 'ROM 檔案'; + + @override + String get patcherPatchFile => '修補檔案'; + + @override + String get patcherOutputFile => '輸出檔案'; + + @override + String get patcherSameFolder => '與 ROM 相同資料夾'; + + @override + String get patcherIgnoreChecksum => '忽略總和檢查錯誤'; + + @override + String get patcherApplyButton => '套用修補'; + + @override + String get patcherSuccess => '修補成功!'; + + @override + String get patcherFormat => '格式'; + + @override + String get patcherChecksumPassed => '通過'; + + @override + String get patcherChecksumSkipped => '略過'; + + @override + String get patcherDropHint => '將 ROM 與修補檔案拖曳至此'; + + @override + String get toolsTitle => '工具'; + + @override + String get compressorTitle => 'ROM 壓縮器'; + + @override + String get compressorAddFiles => '新增檔案'; + + @override + String get compressorAddFolder => '新增資料夾'; + + @override + String get compressorOutputFormat => '輸出格式'; + + @override + String get compressorLevel => '壓縮層級'; + + @override + String get compressorLevelFast => '快速'; + + @override + String get compressorLevelNormal => '標準'; + + @override + String get compressorLevelUltra => '極致'; + + @override + String get compressorDeleteSource => '完成後刪除來源'; + + @override + String get compressorSameFolder => '與來源相同資料夾'; + + @override + String compressorButton(int count) { + return '壓縮($count 個檔案)'; + } + + @override + String get compressorSkipped => '略過(已壓縮)'; + + @override + String get m3uTitle => 'M3U 產生器'; + + @override + String get m3uDropHint => '將光碟檔案拖曳至此'; + + @override + String get m3uOutputLabel => '輸出資料夾'; + + @override + String get m3uButton => '產生 M3U'; + + @override + String m3uSuccess(String filename) { + return '已建立:$filename'; + } + + @override + String get cueGeneratorTitle => 'CUE 產生器'; + + @override + String get cueGeneratorBinFile => 'BIN 檔案'; + + @override + String get cueGeneratorDetectedMode => '偵測模式'; + + @override + String get cueGeneratorButton => '產生 CUE'; + + @override + String cueGeneratorSuccess(String filename) { + return '已建立:$filename'; + } + + @override + String get binMergerTitle => 'BIN 合併器'; + + @override + String get binMergerCueFile => 'CUE 檔案(多音軌)'; + + @override + String binMergerDetected(int count) { + return '偵測到:$count 個 BIN 檔案'; + } + + @override + String get binMergerButton => '合併 BIN'; + + @override + String binMergerSuccess(String filename) { + return '已合併:$filename'; + } + + @override + String get headerRemoverTitle => '檔頭移除器'; + + @override + String get headerRemoverSystem => '系統'; + + @override + String get headerRemoverFound => '找到檔頭'; + + @override + String get headerRemoverNotFound => '找不到檔頭'; + + @override + String get headerRemoverConfidence => '信心度'; + + @override + String get headerRemoverBackup => '保留備份(.bak)'; + + @override + String get headerRemoverButton => '移除檔頭'; + + @override + String headerRemoverSuccess(String filename) { + return '乾淨的 ROM:$filename'; + } + + @override + String get headerRemoverNoHeader => '未偵測到檔頭。'; + + @override + String get dreamcastTitle => 'Dreamcast'; + + @override + String get dreamcastDcpTab => '套用 DCP'; + + @override + String get dreamcastIpbinTab => 'IP.BIN 編輯器'; + + @override + String get dreamcastGdiTab => 'GDI 資訊'; + + @override + String get dreamcastDiscDir => '光碟目錄'; + + @override + String get dreamcastDcpFile => 'DCP 修補檔案'; + + @override + String get dreamcastOutputDir => '輸出目錄'; + + @override + String get dreamcastApplyButton => '套用 DCP 修補'; + + @override + String get dreamcastApplySuccess => 'DCP 修補成功套用!'; + + @override + String get dreamcastIpbinFile => 'IP.BIN 或 GDI 檔案'; + + @override + String get dreamcastIpbinLoad => '載入'; + + @override + String get dreamcastIpbinSave => '儲存變更'; + + @override + String get dreamcastIpbinTitle => '遊戲名稱'; + + @override + String get dreamcastIpbinProductNumber => '產品編號'; + + @override + String get dreamcastIpbinVersion => '版本'; + + @override + String get dreamcastIpbinDate => '發售日期'; + + @override + String get dreamcastIpbinRegion => '區域'; + + @override + String get dreamcastIpbinRegionJapan => '日本'; + + @override + String get dreamcastIpbinRegionUSA => '美國'; + + @override + String get dreamcastIpbinRegionEurope => '歐洲'; + + @override + String get dreamcastIpbinRegionFree => '全區'; + + @override + String get dreamcastIpbinVga => '啟用 VGA'; + + @override + String get dreamcastIpbinSaveSuccess => 'IP.BIN 已成功儲存!'; + + @override + String get dreamcastGdiFile => 'GDI 檔案'; + + @override + String get dreamcastGdiLoad => '載入 GDI'; + + @override + String get dreamcastGdiTracks => '軌道列表'; + + @override + String get dreamcastGdiTrackNum => '軌道'; + + @override + String get dreamcastGdiTrackLba => 'LBA'; + + @override + String get dreamcastGdiTrackType => '類型'; + + @override + String get dreamcastGdiTrackSize => '磁區大小'; + + @override + String get dreamcastGdiTrackFile => '檔案'; + + @override + String get dreamcastGdiAudio => '音訊'; + + @override + String get dreamcastGdiData => '資料'; + + @override + String get dreamcastSameFolder => '與來源相同資料夾'; + + @override + String get dreamcastBrowse => '瀏覽'; + + @override + String get onlineFeaturesSection => '線上功能'; + + @override + String get onlineFeaturesSub => + '所有功能均為選擇性啟用。未經您同意,不會收集任何資料。'; + + @override + String get autoUpdateTitle => '自動檢查更新'; + + @override + String get autoUpdateSub => '啟動時在 GitHub 上檢查新版本'; + + @override + String get autoUpdateCheckNow => '立即檢查'; + + @override + String autoUpdateUpToDate(String version) { + return '已是最新版本($version)'; + } + + @override + String autoUpdateAvailable(String version) { + return '有可用更新:v$version'; + } + + @override + String get autoUpdateDownload => '下載'; + + @override + String get raSection => 'RetroAchievements'; + + @override + String get raEnabledTitle => '啟用 RetroAchievements 雜湊檢查'; + + @override + String get raEnabledSub => '載入前檢查您的 ROM 是否受支援'; + + @override + String get raUsername => '使用者名稱'; + + @override + String get raApiKey => 'API 金鑰'; + + @override + String get raApiKeyHint => '於 retroachievements.org/settings 取得您的金鑰'; + + @override + String get raHashChecking => '檢查雜湊中...'; + + @override + String raHashFound(String title, int count) { + return '✅ 已支援 — $title($count 個成就)'; + } + + @override + String get raHashNotFound => '⚠️ 在 RetroAchievements 上找不到'; + + @override + String raHashError(String msg) { + return '錯誤:$msg'; + } + + @override + String get raViewGame => '在 RA 上檢視'; + + @override + String get raSaveCredentials => '儲存'; +} diff --git a/gui/lib/l10n/app_zh_TW.arb b/gui/lib/l10n/app_zh_TW.arb new file mode 100644 index 0000000..5b6e1f9 --- /dev/null +++ b/gui/lib/l10n/app_zh_TW.arb @@ -0,0 +1,228 @@ +{ + "@@locale": "zh_TW", + "appTitle": "OpenROM", + "appSubtitle": "萬用 ROM 轉換工具", + "convertButton": "轉換", + "convertButtonWithCount": "轉換({count} 個檔案)", + "@convertButtonWithCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "dropZoneHint": "將 ROM 檔案拖曳至此", + "dropZoneSubHint": "或點擊瀏覽", + "settingsTitle": "轉換設定", + "outputFormat": "輸出格式", + "compressionLevel": "壓縮層級", + "compressionNormal": "標準", + "compressionHigh": "高", + "compressionMax": "最高", + "postProcessing": "後續處理", + "verifyAfterConversion": "轉換後驗證", + "outputDestination": "輸出目標", + "browse": "瀏覽", + "statusWaiting": "等待中...", + "statusConverting": "轉換中...", + "statusDone": "完成", + "statusFailed": "失敗", + "aboutTitle": "關於 OpenROM", + "settingsScreen": "設定", + "aboutScreen": "關於", + "themeScreen": "主題", + "errorCoreNotFound": "應用程式資料夾中缺少 openrom-core。", + "errorUnsupportedFormat": "此檔案格式無法使用選擇的輸出格式進行轉換。", + "errorDiskSpace": "可用空間不足以完成此次轉換。", + "errorToolFailed": "轉換工具(chdman/maxcso)未預期結束。", + "errorConversionFailed": "轉換在完成前停止。", + "errorFileNotFound": "輸入檔案已被移動或刪除。", + "errorOutputDirNotFound": "選擇的輸出資料夾已不存在。", + "errorPermissionDenied": "OpenROM 無法寫入輸出資料夾。", + "errorCorruptedFile": "檔案似乎已損毀或為 0 bytes。", + "errorUnknown": "發生錯誤。這可能是程式缺陷。", + "errorDetailsLabel": "詳細資料", + "retryButton": "重試", + "closeButton": "關閉", + "reportBugButton": "回報問題", + "clearQueue": "清除佇列", + "removeFile": "移除", + "openOutputFolder": "開啟輸出資料夾", + "patcherTitle": "ROM 修補器", + "patcherRomFile": "ROM 檔案", + "patcherPatchFile": "修補檔案", + "patcherOutputFile": "輸出檔案", + "patcherSameFolder": "與 ROM 相同資料夾", + "patcherIgnoreChecksum": "忽略總和檢查錯誤", + "patcherApplyButton": "套用修補", + "patcherSuccess": "修補成功!", + "patcherFormat": "格式", + "patcherChecksumPassed": "通過", + "patcherChecksumSkipped": "略過", + "patcherDropHint": "將 ROM 與修補檔案拖曳至此", + "toolsTitle": "工具", + "compressorTitle": "ROM 壓縮器", + "compressorAddFiles": "新增檔案", + "compressorAddFolder": "新增資料夾", + "compressorOutputFormat": "輸出格式", + "compressorLevel": "壓縮層級", + "compressorLevelFast": "快速", + "compressorLevelNormal": "標準", + "compressorLevelUltra": "極致", + "compressorDeleteSource": "完成後刪除來源", + "compressorSameFolder": "與來源相同資料夾", + "compressorButton": "壓縮({count} 個檔案)", + "@compressorButton": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "compressorSkipped": "略過(已壓縮)", + "m3uTitle": "M3U 產生器", + "m3uDropHint": "將光碟檔案拖曳至此", + "m3uOutputLabel": "輸出資料夾", + "m3uButton": "產生 M3U", + "m3uSuccess": "已建立:{filename}", + "@m3uSuccess": { + "placeholders": { + "filename": { + "type": "String" + } + } + }, + "cueGeneratorTitle": "CUE 產生器", + "cueGeneratorBinFile": "BIN 檔案", + "cueGeneratorDetectedMode": "偵測模式", + "cueGeneratorButton": "產生 CUE", + "cueGeneratorSuccess": "已建立:{filename}", + "@cueGeneratorSuccess": { + "placeholders": { + "filename": { + "type": "String" + } + } + }, + "binMergerTitle": "BIN 合併器", + "binMergerCueFile": "CUE 檔案(多音軌)", + "binMergerDetected": "偵測到:{count} 個 BIN 檔案", + "@binMergerDetected": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "binMergerButton": "合併 BIN", + "binMergerSuccess": "已合併:{filename}", + "@binMergerSuccess": { + "placeholders": { + "filename": { + "type": "String" + } + } + }, + "headerRemoverTitle": "檔頭移除器", + "headerRemoverSystem": "系統", + "headerRemoverFound": "找到檔頭", + "headerRemoverNotFound": "找不到檔頭", + "headerRemoverConfidence": "信心度", + "headerRemoverBackup": "保留備份(.bak)", + "headerRemoverButton": "移除檔頭", + "headerRemoverSuccess": "乾淨的 ROM:{filename}", + "@headerRemoverSuccess": { + "placeholders": { + "filename": { + "type": "String" + } + } + }, + "headerRemoverNoHeader": "未偵測到檔頭。", + "dreamcastTitle": "Dreamcast", + "dreamcastDcpTab": "套用 DCP", + "dreamcastIpbinTab": "IP.BIN 編輯器", + "dreamcastGdiTab": "GDI 資訊", + "dreamcastDiscDir": "光碟目錄", + "dreamcastDcpFile": "DCP 修補檔案", + "dreamcastOutputDir": "輸出目錄", + "dreamcastApplyButton": "套用 DCP 修補", + "dreamcastApplySuccess": "DCP 修補成功套用!", + "dreamcastIpbinFile": "IP.BIN 或 GDI 檔案", + "dreamcastIpbinLoad": "載入", + "dreamcastIpbinSave": "儲存變更", + "dreamcastIpbinTitle": "遊戲名稱", + "dreamcastIpbinProductNumber": "產品編號", + "dreamcastIpbinVersion": "版本", + "dreamcastIpbinDate": "發售日期", + "dreamcastIpbinRegion": "區域", + "dreamcastIpbinRegionJapan": "日本", + "dreamcastIpbinRegionUSA": "美國", + "dreamcastIpbinRegionEurope": "歐洲", + "dreamcastIpbinRegionFree": "全區", + "dreamcastIpbinVga": "啟用 VGA", + "dreamcastIpbinSaveSuccess": "IP.BIN 已成功儲存!", + "dreamcastGdiFile": "GDI 檔案", + "dreamcastGdiLoad": "載入 GDI", + "dreamcastGdiTracks": "軌道列表", + "dreamcastGdiTrackNum": "軌道", + "dreamcastGdiTrackLba": "LBA", + "dreamcastGdiTrackType": "類型", + "dreamcastGdiTrackSize": "磁區大小", + "dreamcastGdiTrackFile": "檔案", + "dreamcastGdiAudio": "音訊", + "dreamcastGdiData": "資料", + "dreamcastSameFolder": "與來源相同資料夾", + "dreamcastBrowse": "瀏覽", + "onlineFeaturesSection": "線上功能", + "onlineFeaturesSub": "所有功能均為選擇性啟用。未經您同意,不會收集任何資料。", + "autoUpdateTitle": "自動檢查更新", + "autoUpdateSub": "啟動時在 GitHub 上檢查新版本", + "autoUpdateCheckNow": "立即檢查", + "autoUpdateUpToDate": "已是最新版本({version})", + "@autoUpdateUpToDate": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "autoUpdateAvailable": "有可用更新:v{version}", + "@autoUpdateAvailable": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "autoUpdateDownload": "下載", + "raSection": "RetroAchievements", + "raEnabledTitle": "啟用 RetroAchievements 雜湊檢查", + "raEnabledSub": "載入前檢查您的 ROM 是否受支援", + "raUsername": "使用者名稱", + "raApiKey": "API 金鑰", + "raApiKeyHint": "於 retroachievements.org/settings 取得您的金鑰", + "raHashChecking": "檢查雜湊中...", + "raHashFound": "✅ 已支援 — {title}({count} 個成就)", + "@raHashFound": { + "placeholders": { + "title": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "raHashNotFound": "⚠️ 在 RetroAchievements 上找不到", + "raHashError": "錯誤:{msg}", + "@raHashError": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "raViewGame": "在 RA 上檢視", + "raSaveCredentials": "儲存" +} diff --git a/gui/lib/providers/locale_provider.dart b/gui/lib/providers/locale_provider.dart index 2f76acd..d73f494 100644 --- a/gui/lib/providers/locale_provider.dart +++ b/gui/lib/providers/locale_provider.dart @@ -14,12 +14,21 @@ class LocaleProvider extends ChangeNotifier { Locale('fr'), Locale('ja'), Locale('pt'), + Locale('zh'), + Locale('zh', 'TW'), ]; Locale _locale = const Locale('en'); Locale get locale => _locale; + String get _localeCode { + if (_locale.countryCode != null && _locale.countryCode!.isNotEmpty) { + return '${_locale.languageCode}_${_locale.countryCode}'; + } + return _locale.languageCode; + } + LocaleProvider() { _loadLocale(); } @@ -31,6 +40,17 @@ class LocaleProvider extends ChangeNotifier { if (supportedLocales.any((l) => l.languageCode == code)) { _locale = Locale(code); notifyListeners(); + } else if (code.contains('_')) { + final parts = code.split('_'); + if (parts.length == 2) { + final locale = Locale(parts[0], parts[1]); + if (supportedLocales.any((l) => + l.languageCode == locale.languageCode && + l.countryCode == locale.countryCode)) { + _locale = locale; + notifyListeners(); + } + } } } } @@ -46,7 +66,7 @@ class LocaleProvider extends ChangeNotifier { notifyListeners(); final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_prefKey, newLocale.languageCode); + await prefs.setString(_prefKey, _localeCode); } static String getNativeName(String code) { @@ -61,9 +81,13 @@ class LocaleProvider extends ChangeNotifier { return '日本語'; case 'pt': return 'Português'; + case 'zh': + return '简体中文'; + case 'zh_TW': + return '繁體中文'; case 'en': default: return 'English'; } } -} +} \ No newline at end of file From 7659af3f3f617d9786276b7b4a83eaeeb9b7d131 Mon Sep 17 00:00:00 2001 From: Ain Date: Sun, 11 Oct 2026 10:52:16 +0800 Subject: [PATCH 2/3] feat(i18n): complete Simplified Chinese (zh) localization --- gui/lib/l10n/app_localizations_zh.dart | 283 ++++++++++++------------ gui/lib/l10n/app_zh.arb | 291 +++++++++++++++---------- 2 files changed, 309 insertions(+), 265 deletions(-) diff --git a/gui/lib/l10n/app_localizations_zh.dart b/gui/lib/l10n/app_localizations_zh.dart index 124da41..5cff621 100644 --- a/gui/lib/l10n/app_localizations_zh.dart +++ b/gui/lib/l10n/app_localizations_zh.dart @@ -12,367 +12,360 @@ class AppLocalizationsZh extends AppLocalizations { String get appTitle => 'OpenROM'; @override - String get appSubtitle => 'Universal ROM Conversion Suite'; + String get appSubtitle => '通用 ROM 转换套件'; @override - String get convertButton => 'Convert'; + String get convertButton => '转换'; @override String convertButtonWithCount(int count) { - return 'Convert ($count Files)'; + return '转换($count 个文件)'; } @override - String get dropZoneHint => 'Drag & Drop ROM files here'; + String get dropZoneHint => '将 ROM 文件拖放到此处'; @override - String get dropZoneSubHint => 'or Click to browse'; + String get dropZoneSubHint => '或点击浏览'; @override - String get settingsTitle => 'Conversion Settings'; + String get settingsTitle => '转换设置'; @override - String get outputFormat => 'Output Format'; + String get outputFormat => '输出格式'; @override - String get compressionLevel => 'Compression Level'; + String get compressionLevel => '压缩级别'; @override - String get compressionNormal => 'Normal'; + String get compressionNormal => '标准'; @override - String get compressionHigh => 'High'; + String get compressionHigh => '高'; @override - String get compressionMax => 'Max'; + String get compressionMax => '最大'; @override - String get postProcessing => 'Post-Processing'; + String get postProcessing => '后处理'; @override - String get verifyAfterConversion => 'Verify after conversion'; + String get verifyAfterConversion => '转换后验证'; @override - String get outputDestination => 'Output Destination'; + String get outputDestination => '输出目标'; @override - String get browse => 'Browse'; + String get browse => '浏览'; @override - String get statusWaiting => 'Waiting...'; + String get statusWaiting => '等待中...'; @override - String get statusConverting => 'Converting...'; + String get statusConverting => '转换中...'; @override - String get statusDone => 'Done'; + String get statusDone => '完成'; @override - String get statusFailed => 'Failed'; + String get statusFailed => '失败'; @override - String get aboutTitle => 'About OpenROM'; + String get aboutTitle => '关于 OpenROM'; @override - String get settingsScreen => 'Settings'; + String get settingsScreen => '设置'; @override - String get aboutScreen => 'About'; + String get aboutScreen => '关于'; @override - String get themeScreen => 'Themes'; + String get themeScreen => '主题'; @override - String get errorCoreNotFound => - 'openrom-core is missing from the app folder.'; + String get errorCoreNotFound => '应用程序文件夹中缺少 openrom-core。'; @override - String get errorUnsupportedFormat => - 'This file format cannot be converted with the selected output.'; + String get errorUnsupportedFormat => '此文件格式无法使用所选输出进行转换。'; @override - String get errorDiskSpace => - 'Not enough free space to complete this conversion.'; + String get errorDiskSpace => '可用空间不足以完成此转换。'; @override - String get errorToolFailed => - 'The conversion tool (chdman/maxcso) exited unexpectedly.'; + String get errorToolFailed => '转换工具(chdman/maxcso)意外退出。'; @override - String get errorConversionFailed => 'Conversion stopped before completing.'; + String get errorConversionFailed => '转换在完成前停止。'; @override - String get errorFileNotFound => 'The input file was moved or deleted.'; + String get errorFileNotFound => '输入文件已被移动或删除。'; @override - String get errorOutputDirNotFound => - 'The selected output folder no longer exists.'; + String get errorOutputDirNotFound => '所选输出文件夹已不存在。'; @override - String get errorPermissionDenied => - 'OpenROM can\'t write to the output folder.'; + String get errorPermissionDenied => 'OpenROM 无法写入输出文件夹。'; @override - String get errorCorruptedFile => - 'The file appears to be corrupted or is 0 bytes.'; + String get errorCorruptedFile => '文件似乎已损坏或为 0 字节。'; @override - String get errorUnknown => 'Something went wrong. This might be a bug.'; + String get errorUnknown => '发生错误。这可能是程序缺陷。'; @override - String get errorDetailsLabel => 'Details'; + String get errorDetailsLabel => '详细信息'; @override - String get retryButton => 'Retry'; + String get retryButton => '重试'; @override - String get closeButton => 'Close'; + String get closeButton => '关闭'; @override - String get reportBugButton => 'Report Bug'; + String get reportBugButton => '报告问题'; @override - String get clearQueue => 'Clear Queue'; + String get clearQueue => '清除队列'; @override - String get removeFile => 'Remove'; + String get removeFile => '移除'; @override - String get openOutputFolder => 'Open Output Folder'; + String get openOutputFolder => '打开输出文件夹'; @override - String get patcherTitle => 'ROM Patcher'; + String get patcherTitle => 'ROM 修补器'; @override - String get patcherRomFile => 'ROM File'; + String get patcherRomFile => 'ROM 文件'; @override - String get patcherPatchFile => 'Patch File'; + String get patcherPatchFile => '补丁文件'; @override - String get patcherOutputFile => 'Output File'; + String get patcherOutputFile => '输出文件'; @override - String get patcherSameFolder => 'Same folder as ROM'; + String get patcherSameFolder => '与 ROM 相同文件夹'; @override - String get patcherIgnoreChecksum => 'Ignore checksum errors'; + String get patcherIgnoreChecksum => '忽略校验和错误'; @override - String get patcherApplyButton => 'Apply Patch'; + String get patcherApplyButton => '应用补丁'; @override - String get patcherSuccess => 'Patched successfully!'; + String get patcherSuccess => '修补成功!'; @override - String get patcherFormat => 'Format'; + String get patcherFormat => '格式'; @override String get patcherChecksumPassed => '通过'; @override - String get patcherChecksumSkipped => 'skipped'; + String get patcherChecksumSkipped => '已跳过'; @override - String get patcherDropHint => 'Drop ROM + Patch files here'; + String get patcherDropHint => '将 ROM 和补丁文件拖放到此处'; @override - String get toolsTitle => 'Tools'; + String get toolsTitle => '工具'; @override - String get compressorTitle => 'ROM Compressor'; + String get compressorTitle => 'ROM 压缩器'; @override - String get compressorAddFiles => 'Add Files'; + String get compressorAddFiles => '添加文件'; @override - String get compressorAddFolder => 'Add Folder'; + String get compressorAddFolder => '添加文件夹'; @override - String get compressorOutputFormat => 'Output Format'; + String get compressorOutputFormat => '输出格式'; @override - String get compressorLevel => 'Compression Level'; + String get compressorLevel => '压缩级别'; @override - String get compressorLevelFast => 'Fast'; + String get compressorLevelFast => '快速'; @override - String get compressorLevelNormal => 'Normal'; + String get compressorLevelNormal => '标准'; @override - String get compressorLevelUltra => 'Ultra'; + String get compressorLevelUltra => '极致'; @override - String get compressorDeleteSource => 'Delete source after done'; + String get compressorDeleteSource => '完成后删除源文件'; @override - String get compressorSameFolder => 'Same folder as source'; + String get compressorSameFolder => '与源文件相同文件夹'; @override String compressorButton(int count) { - return 'Compress ($count Files)'; + return '压缩($count 个文件)'; } @override - String get compressorSkipped => 'Skipped (already compressed)'; + String get compressorSkipped => '已跳过(已压缩)'; @override - String get m3uTitle => 'M3U Generator'; + String get m3uTitle => 'M3U 生成器'; @override - String get m3uDropHint => 'Drop disc files here'; + String get m3uDropHint => '将光盘文件拖放到此处'; @override - String get m3uOutputLabel => 'Output folder'; + String get m3uOutputLabel => '输出文件夹'; @override - String get m3uButton => 'Generate M3U'; + String get m3uButton => '生成 M3U'; @override String m3uSuccess(String filename) { - return 'Created: $filename'; + return '已创建:$filename'; } @override - String get cueGeneratorTitle => 'CUE Generator'; + String get cueGeneratorTitle => 'CUE 生成器'; @override - String get cueGeneratorBinFile => 'BIN File'; + String get cueGeneratorBinFile => 'BIN 文件'; @override - String get cueGeneratorDetectedMode => 'Detected mode'; + String get cueGeneratorDetectedMode => '检测模式'; @override - String get cueGeneratorButton => 'Generate CUE'; + String get cueGeneratorButton => '生成 CUE'; @override String cueGeneratorSuccess(String filename) { - return 'Created: $filename'; + return '已创建:$filename'; } @override - String get binMergerTitle => 'BIN Merger'; + String get binMergerTitle => 'BIN 合并器'; @override - String get binMergerCueFile => 'CUE File (multi-track)'; + String get binMergerCueFile => 'CUE 文件(多轨道)'; @override String binMergerDetected(int count) { - return 'Detected: $count BIN files'; + return '检测到:$count 个 BIN 文件'; } @override - String get binMergerButton => 'Merge BINs'; + String get binMergerButton => '合并 BIN'; @override String binMergerSuccess(String filename) { - return 'Merged: $filename'; + return '已合并:$filename'; } @override - String get headerRemoverTitle => 'Header Remover'; + String get headerRemoverTitle => '文件头移除器'; @override - String get headerRemoverSystem => 'System'; + String get headerRemoverSystem => '系统'; @override - String get headerRemoverFound => 'Header Found'; + String get headerRemoverFound => '找到文件头'; @override - String get headerRemoverNotFound => 'No Header Found'; + String get headerRemoverNotFound => '未找到文件头'; @override - String get headerRemoverConfidence => 'Confidence'; + String get headerRemoverConfidence => '置信度'; @override - String get headerRemoverBackup => 'Keep backup (.bak)'; + String get headerRemoverBackup => '保留备份(.bak)'; @override - String get headerRemoverButton => 'Remove Header'; + String get headerRemoverButton => '移除文件头'; @override String headerRemoverSuccess(String filename) { - return 'Clean ROM: $filename'; + return '干净的 ROM:$filename'; } @override - String get headerRemoverNoHeader => 'No copier header detected.'; + String get headerRemoverNoHeader => '未检测到文件头。'; @override String get dreamcastTitle => 'Dreamcast'; @override - String get dreamcastDcpTab => 'Apply DCP'; + String get dreamcastDcpTab => '应用 DCP'; @override - String get dreamcastIpbinTab => 'IP.BIN Editor'; + String get dreamcastIpbinTab => 'IP.BIN 编辑器'; @override - String get dreamcastGdiTab => 'GDI Info'; + String get dreamcastGdiTab => 'GDI 信息'; @override - String get dreamcastDiscDir => 'Disc Directory'; + String get dreamcastDiscDir => '光盘目录'; @override - String get dreamcastDcpFile => 'DCP Patch File'; + String get dreamcastDcpFile => 'DCP 补丁文件'; @override - String get dreamcastOutputDir => 'Output Directory'; + String get dreamcastOutputDir => '输出目录'; @override - String get dreamcastApplyButton => 'Apply DCP Patch'; + String get dreamcastApplyButton => '应用 DCP 补丁'; @override - String get dreamcastApplySuccess => 'DCP patch applied successfully!'; + String get dreamcastApplySuccess => 'DCP 补丁应用成功!'; @override - String get dreamcastIpbinFile => 'IP.BIN or GDI File'; + String get dreamcastIpbinFile => 'IP.BIN 或 GDI 文件'; @override - String get dreamcastIpbinLoad => 'Load'; + String get dreamcastIpbinLoad => '加载'; @override - String get dreamcastIpbinSave => 'Save Changes'; + String get dreamcastIpbinSave => '保存更改'; @override - String get dreamcastIpbinTitle => 'Game Title'; + String get dreamcastIpbinTitle => '游戏名称'; @override - String get dreamcastIpbinProductNumber => 'Product Number'; + String get dreamcastIpbinProductNumber => '产品编号'; @override - String get dreamcastIpbinVersion => 'Version'; + String get dreamcastIpbinVersion => '版本'; @override - String get dreamcastIpbinDate => 'Release Date'; + String get dreamcastIpbinDate => '发行日期'; @override - String get dreamcastIpbinRegion => 'Region'; + String get dreamcastIpbinRegion => '区域'; @override - String get dreamcastIpbinRegionJapan => 'Japan'; + String get dreamcastIpbinRegionJapan => '日本'; @override - String get dreamcastIpbinRegionUSA => 'USA'; + String get dreamcastIpbinRegionUSA => '美国'; @override - String get dreamcastIpbinRegionEurope => 'Europe'; + String get dreamcastIpbinRegionEurope => '欧洲'; @override - String get dreamcastIpbinRegionFree => 'Region Free'; + String get dreamcastIpbinRegionFree => '全区'; @override - String get dreamcastIpbinVga => 'Enable VGA'; + String get dreamcastIpbinVga => '启用 VGA'; @override - String get dreamcastIpbinSaveSuccess => 'IP.BIN saved successfully!'; + String get dreamcastIpbinSaveSuccess => 'IP.BIN 保存成功!'; @override - String get dreamcastGdiFile => 'GDI File'; + String get dreamcastGdiFile => 'GDI 文件'; @override - String get dreamcastGdiLoad => 'Load GDI'; + String get dreamcastGdiLoad => '加载 GDI'; @override String get dreamcastGdiTracks => '轨道列表'; @@ -384,92 +377,92 @@ class AppLocalizationsZh extends AppLocalizations { String get dreamcastGdiTrackLba => 'LBA'; @override - String get dreamcastGdiTrackType => 'Type'; + String get dreamcastGdiTrackType => '类型'; @override - String get dreamcastGdiTrackSize => 'Sector Size'; + String get dreamcastGdiTrackSize => '扇区大小'; @override - String get dreamcastGdiTrackFile => 'File'; + String get dreamcastGdiTrackFile => '文件'; @override - String get dreamcastGdiAudio => 'Audio'; + String get dreamcastGdiAudio => '音频'; @override - String get dreamcastGdiData => 'Data'; + String get dreamcastGdiData => '数据'; @override - String get dreamcastSameFolder => 'Same folder as source'; + String get dreamcastSameFolder => '与源文件相同文件夹'; @override String get dreamcastBrowse => '浏览'; @override - String get onlineFeaturesSection => 'Online Features'; + String get onlineFeaturesSection => '在线功能'; @override - String get onlineFeaturesSub => - 'All features are opt-in. No data is collected without your consent.'; + String get onlineFeaturesSub => '所有功能均为可选启用。未经您的同意,不会收集任何数据。'; @override - String get autoUpdateTitle => 'Check for updates automatically'; + String get autoUpdateTitle => '自动检查更新'; @override - String get autoUpdateSub => 'Checks GitHub on startup for a newer version'; + String get autoUpdateSub => '启动时在 GitHub 上检查新版本'; @override - String get autoUpdateCheckNow => 'Check now'; + String get autoUpdateCheckNow => '立即检查'; @override String autoUpdateUpToDate(String version) { - return 'You\'re up to date ($version)'; + return '已是最新版本($version)'; } @override String autoUpdateAvailable(String version) { - return 'Update available: v$version'; + return '有可用更新:v$version'; } @override - String get autoUpdateDownload => 'Download'; + String get autoUpdateDownload => '下载'; @override String get raSection => 'RetroAchievements'; @override - String get raEnabledTitle => 'Enable RetroAchievements hash check'; + String get raEnabledTitle => '启用 RetroAchievements 哈希检查'; @override - String get raEnabledSub => 'Checks if your ROM is supported before loading'; + String get raEnabledSub => '加载前检查您的 ROM 是否受支持'; @override - String get raUsername => 'Username'; + String get raUsername => '用户名'; @override - String get raApiKey => 'API Key'; + String get raApiKey => 'API 密钥'; @override - String get raApiKeyHint => 'Get your key at retroachievements.org/settings'; + String get raApiKeyHint => '在 retroachievements.org/settings 获取您的密钥'; @override - String get raHashChecking => 'Checking hash...'; + String get raHashChecking => '正在检查哈希值...'; @override String raHashFound(String title, int count) { - return '✅ Supported — $title ($count achievements)'; + return '✅ 已支持 — $title($count 个成就)'; } @override - String get raHashNotFound => '⚠️ Not found on RetroAchievements'; + String get raHashNotFound => '⚠️ 在 RetroAchievements 上未找到'; @override String raHashError(String msg) { - return 'Error: $msg'; + return '错误:$msg'; } @override - String get raViewGame => 'View on RA'; + String get raViewGame => '在 RA 上查看'; @override - String get raSaveCredentials => 'Save'; + String get raSaveCredentials => '保存'; } + diff --git a/gui/lib/l10n/app_zh.arb b/gui/lib/l10n/app_zh.arb index ecbc4cd..3f338f4 100644 --- a/gui/lib/l10n/app_zh.arb +++ b/gui/lib/l10n/app_zh.arb @@ -1,9 +1,9 @@ { "@@locale": "zh", "appTitle": "OpenROM", - "appSubtitle": "Universal ROM Conversion Suite", - "convertButton": "Convert", - "convertButtonWithCount": "Convert ({count} Files)", + "appSubtitle": "通用 ROM 转换套件", + "convertButton": "转换", + "convertButtonWithCount": "转换({count} 个文件)", "@convertButtonWithCount": { "placeholders": { "count": { @@ -11,67 +11,67 @@ } } }, - "dropZoneHint": "Drag & Drop ROM files here", - "dropZoneSubHint": "or Click to browse", - "settingsTitle": "Conversion Settings", - "outputFormat": "Output Format", - "compressionLevel": "Compression Level", - "compressionNormal": "Normal", - "compressionHigh": "High", - "compressionMax": "Max", - "postProcessing": "Post-Processing", - "verifyAfterConversion": "Verify after conversion", - "outputDestination": "Output Destination", - "browse": "Browse", - "statusWaiting": "Waiting...", - "statusConverting": "Converting...", - "statusDone": "Done", - "statusFailed": "Failed", - "aboutTitle": "About OpenROM", - "settingsScreen": "Settings", - "aboutScreen": "About", - "themeScreen": "Themes", - "errorCoreNotFound": "openrom-core is missing from the app folder.", - "errorUnsupportedFormat": "This file format cannot be converted with the selected output.", - "errorDiskSpace": "Not enough free space to complete this conversion.", - "errorToolFailed": "The conversion tool (chdman/maxcso) exited unexpectedly.", - "errorConversionFailed": "Conversion stopped before completing.", - "errorFileNotFound": "The input file was moved or deleted.", - "errorOutputDirNotFound": "The selected output folder no longer exists.", - "errorPermissionDenied": "OpenROM can't write to the output folder.", - "errorCorruptedFile": "The file appears to be corrupted or is 0 bytes.", - "errorUnknown": "Something went wrong. This might be a bug.", - "errorDetailsLabel": "Details", - "retryButton": "Retry", - "closeButton": "Close", - "reportBugButton": "Report Bug", - "clearQueue": "Clear Queue", - "removeFile": "Remove", - "openOutputFolder": "Open Output Folder", - "patcherTitle": "ROM Patcher", - "patcherRomFile": "ROM File", - "patcherPatchFile": "Patch File", - "patcherOutputFile": "Output File", - "patcherSameFolder": "Same folder as ROM", - "patcherIgnoreChecksum": "Ignore checksum errors", - "patcherApplyButton": "Apply Patch", - "patcherSuccess": "Patched successfully!", - "patcherFormat": "Format", + "dropZoneHint": "将 ROM 文件拖放到此处", + "dropZoneSubHint": "或点击浏览", + "settingsTitle": "转换设置", + "outputFormat": "输出格式", + "compressionLevel": "压缩级别", + "compressionNormal": "标准", + "compressionHigh": "高", + "compressionMax": "最大", + "postProcessing": "后处理", + "verifyAfterConversion": "转换后验证", + "outputDestination": "输出目标", + "browse": "浏览", + "statusWaiting": "等待中...", + "statusConverting": "转换中...", + "statusDone": "完成", + "statusFailed": "失败", + "aboutTitle": "关于 OpenROM", + "settingsScreen": "设置", + "aboutScreen": "关于", + "themeScreen": "主题", + "errorCoreNotFound": "应用程序文件夹中缺少 openrom-core。", + "errorUnsupportedFormat": "此文件格式无法使用所选输出进行转换。", + "errorDiskSpace": "可用空间不足以完成此转换。", + "errorToolFailed": "转换工具(chdman/maxcso)意外退出。", + "errorConversionFailed": "转换在完成前停止。", + "errorFileNotFound": "输入文件已被移动或删除。", + "errorOutputDirNotFound": "所选输出文件夹已不存在。", + "errorPermissionDenied": "OpenROM 无法写入输出文件夹。", + "errorCorruptedFile": "文件似乎已损坏或为 0 字节。", + "errorUnknown": "发生错误。这可能是程序缺陷。", + "errorDetailsLabel": "详细信息", + "retryButton": "重试", + "closeButton": "关闭", + "reportBugButton": "报告问题", + "clearQueue": "清除队列", + "removeFile": "移除", + "openOutputFolder": "打开输出文件夹", + "patcherTitle": "ROM 修补器", + "patcherRomFile": "ROM 文件", + "patcherPatchFile": "补丁文件", + "patcherOutputFile": "输出文件", + "patcherSameFolder": "与 ROM 相同文件夹", + "patcherIgnoreChecksum": "忽略校验和错误", + "patcherApplyButton": "应用补丁", + "patcherSuccess": "修补成功!", + "patcherFormat": "格式", "patcherChecksumPassed": "通过", - "patcherChecksumSkipped": "skipped", - "patcherDropHint": "Drop ROM + Patch files here", - "toolsTitle": "Tools", - "compressorTitle": "ROM Compressor", - "compressorAddFiles": "Add Files", - "compressorAddFolder": "Add Folder", - "compressorOutputFormat": "Output Format", - "compressorLevel": "Compression Level", - "compressorLevelFast": "Fast", - "compressorLevelNormal": "Normal", - "compressorLevelUltra": "Ultra", - "compressorDeleteSource": "Delete source after done", - "compressorSameFolder": "Same folder as source", - "compressorButton": "Compress ({count} Files)", + "patcherChecksumSkipped": "已跳过", + "patcherDropHint": "将 ROM 和补丁文件拖放到此处", + "toolsTitle": "工具", + "compressorTitle": "ROM 压缩器", + "compressorAddFiles": "添加文件", + "compressorAddFolder": "添加文件夹", + "compressorOutputFormat": "输出格式", + "compressorLevel": "压缩级别", + "compressorLevelFast": "快速", + "compressorLevelNormal": "标准", + "compressorLevelUltra": "极致", + "compressorDeleteSource": "完成后删除源文件", + "compressorSameFolder": "与源文件相同文件夹", + "compressorButton": "压缩({count} 个文件)", "@compressorButton": { "placeholders": { "count": { @@ -79,12 +79,12 @@ } } }, - "compressorSkipped": "Skipped (already compressed)", - "m3uTitle": "M3U Generator", - "m3uDropHint": "Drop disc files here", - "m3uOutputLabel": "Output folder", - "m3uButton": "Generate M3U", - "m3uSuccess": "Created: {filename}", + "compressorSkipped": "已跳过(已压缩)", + "m3uTitle": "M3U 生成器", + "m3uDropHint": "将光盘文件拖放到此处", + "m3uOutputLabel": "输出文件夹", + "m3uButton": "生成 M3U", + "m3uSuccess": "已创建:{filename}", "@m3uSuccess": { "placeholders": { "filename": { @@ -92,11 +92,11 @@ } } }, - "cueGeneratorTitle": "CUE Generator", - "cueGeneratorBinFile": "BIN File", - "cueGeneratorDetectedMode": "Detected mode", - "cueGeneratorButton": "Generate CUE", - "cueGeneratorSuccess": "Created: {filename}", + "cueGeneratorTitle": "CUE 生成器", + "cueGeneratorBinFile": "BIN 文件", + "cueGeneratorDetectedMode": "检测模式", + "cueGeneratorButton": "生成 CUE", + "cueGeneratorSuccess": "已创建:{filename}", "@cueGeneratorSuccess": { "placeholders": { "filename": { @@ -104,9 +104,9 @@ } } }, - "binMergerTitle": "BIN Merger", - "binMergerCueFile": "CUE File (multi-track)", - "binMergerDetected": "Detected: {count} BIN files", + "binMergerTitle": "BIN 合并器", + "binMergerCueFile": "CUE 文件(多轨道)", + "binMergerDetected": "检测到:{count} 个 BIN 文件", "@binMergerDetected": { "placeholders": { "count": { @@ -114,8 +114,8 @@ } } }, - "binMergerButton": "Merge BINs", - "binMergerSuccess": "Merged: {filename}", + "binMergerButton": "合并 BIN", + "binMergerSuccess": "已合并:{filename}", "@binMergerSuccess": { "placeholders": { "filename": { @@ -123,14 +123,14 @@ } } }, - "headerRemoverTitle": "Header Remover", - "headerRemoverSystem": "System", - "headerRemoverFound": "Header Found", - "headerRemoverNotFound": "No Header Found", - "headerRemoverConfidence": "Confidence", - "headerRemoverBackup": "Keep backup (.bak)", - "headerRemoverButton": "Remove Header", - "headerRemoverSuccess": "Clean ROM: {filename}", + "headerRemoverTitle": "文件头移除器", + "headerRemoverSystem": "系统", + "headerRemoverFound": "找到文件头", + "headerRemoverNotFound": "未找到文件头", + "headerRemoverConfidence": "置信度", + "headerRemoverBackup": "保留备份(.bak)", + "headerRemoverButton": "移除文件头", + "headerRemoverSuccess": "干净的 ROM:{filename}", "@headerRemoverSuccess": { "placeholders": { "filename": { @@ -138,40 +138,91 @@ } } }, - "headerRemoverNoHeader": "No copier header detected.", + "headerRemoverNoHeader": "未检测到文件头。", "dreamcastTitle": "Dreamcast", - "dreamcastDcpTab": "Apply DCP", - "dreamcastIpbinTab": "IP.BIN Editor", - "dreamcastGdiTab": "GDI Info", - "dreamcastDiscDir": "Disc Directory", - "dreamcastDcpFile": "DCP Patch File", - "dreamcastOutputDir": "Output Directory", - "dreamcastApplyButton": "Apply DCP Patch", - "dreamcastApplySuccess": "DCP patch applied successfully!", - "dreamcastIpbinFile": "IP.BIN or GDI File", - "dreamcastIpbinLoad": "Load", - "dreamcastIpbinSave": "Save Changes", - "dreamcastIpbinTitle": "Game Title", - "dreamcastIpbinProductNumber": "Product Number", - "dreamcastIpbinVersion": "Version", - "dreamcastIpbinDate": "Release Date", - "dreamcastIpbinRegion": "Region", - "dreamcastIpbinRegionJapan": "Japan", - "dreamcastIpbinRegionUSA": "USA", - "dreamcastIpbinRegionEurope": "Europe", - "dreamcastIpbinRegionFree": "Region Free", - "dreamcastIpbinVga": "Enable VGA", - "dreamcastIpbinSaveSuccess": "IP.BIN saved successfully!", - "dreamcastGdiFile": "GDI File", - "dreamcastGdiLoad": "Load GDI", + "dreamcastDcpTab": "应用 DCP", + "dreamcastIpbinTab": "IP.BIN 编辑器", + "dreamcastGdiTab": "GDI 信息", + "dreamcastDiscDir": "光盘目录", + "dreamcastDcpFile": "DCP 补丁文件", + "dreamcastOutputDir": "输出目录", + "dreamcastApplyButton": "应用 DCP 补丁", + "dreamcastApplySuccess": "DCP 补丁应用成功!", + "dreamcastIpbinFile": "IP.BIN 或 GDI 文件", + "dreamcastIpbinLoad": "加载", + "dreamcastIpbinSave": "保存更改", + "dreamcastIpbinTitle": "游戏名称", + "dreamcastIpbinProductNumber": "产品编号", + "dreamcastIpbinVersion": "版本", + "dreamcastIpbinDate": "发行日期", + "dreamcastIpbinRegion": "区域", + "dreamcastIpbinRegionJapan": "日本", + "dreamcastIpbinRegionUSA": "美国", + "dreamcastIpbinRegionEurope": "欧洲", + "dreamcastIpbinRegionFree": "全区", + "dreamcastIpbinVga": "启用 VGA", + "dreamcastIpbinSaveSuccess": "IP.BIN 保存成功!", + "dreamcastGdiFile": "GDI 文件", + "dreamcastGdiLoad": "加载 GDI", "dreamcastGdiTracks": "轨道列表", "dreamcastGdiTrackNum": "轨道", "dreamcastGdiTrackLba": "LBA", - "dreamcastGdiTrackType": "Type", - "dreamcastGdiTrackSize": "Sector Size", - "dreamcastGdiTrackFile": "File", - "dreamcastGdiAudio": "Audio", - "dreamcastGdiData": "Data", - "dreamcastSameFolder": "Same folder as source", - "dreamcastBrowse": "浏览" -} \ No newline at end of file + "dreamcastGdiTrackType": "类型", + "dreamcastGdiTrackSize": "扇区大小", + "dreamcastGdiTrackFile": "文件", + "dreamcastGdiAudio": "音频", + "dreamcastGdiData": "数据", + "dreamcastSameFolder": "与源文件相同文件夹", + "dreamcastBrowse": "浏览", + "onlineFeaturesSection": "在线功能", + "onlineFeaturesSub": "所有功能均为可选启用。未经您的同意,不会收集任何数据。", + "autoUpdateTitle": "自动检查更新", + "autoUpdateSub": "启动时在 GitHub 上检查新版本", + "autoUpdateCheckNow": "立即检查", + "autoUpdateUpToDate": "已是最新版本({version})", + "@autoUpdateUpToDate": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "autoUpdateAvailable": "有可用更新:v{version}", + "@autoUpdateAvailable": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "autoUpdateDownload": "下载", + "raSection": "RetroAchievements", + "raEnabledTitle": "启用 RetroAchievements 哈希检查", + "raEnabledSub": "加载前检查您的 ROM 是否受支持", + "raUsername": "用户名", + "raApiKey": "API 密钥", + "raApiKeyHint": "在 retroachievements.org/settings 获取您的密钥", + "raHashChecking": "正在检查哈希值...", + "raHashFound": "✅ 已支持 — {title}({count} 个成就)", + "@raHashFound": { + "placeholders": { + "title": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "raHashNotFound": "⚠️ 在 RetroAchievements 上未找到", + "raHashError": "错误:{msg}", + "@raHashError": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "raViewGame": "在 RA 上查看", + "raSaveCredentials": "保存" +} From 5f9336128d3a92ded8d2f11398385d6ed807bc6d Mon Sep 17 00:00:00 2001 From: Ain Date: Sun, 11 Oct 2026 11:23:04 +0800 Subject: [PATCH 3/3] fix(i18n): align terminology and run dart format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use 壓縮等級/压缩等级 instead of 壓縮層級/压缩级别 - Use 輸出位置/输出位置 instead of 輸出目標/输出目标 - Use Bug instead of 程式缺陷/程序缺陷 - Run dart format to pass CI quality gate --- gui/lib/app.dart | 21 +- gui/lib/controllers/home_controller.dart | 13 +- gui/lib/l10n/app_localizations.dart | 51 +++-- gui/lib/l10n/app_localizations_zh.dart | 9 +- gui/lib/l10n/app_localizations_zh_TW.dart | 32 +-- gui/lib/l10n/app_zh.arb | 8 +- gui/lib/l10n/app_zh_TW.arb | 8 +- gui/lib/models/conversion_job.dart | 4 +- gui/lib/models/rom_file.dart | 6 +- gui/lib/patcher/aps_gba_builder.dart | 10 +- gui/lib/patcher/aps_n64_patcher.dart | 9 +- gui/lib/patcher/aps_patcher.dart | 16 +- gui/lib/patcher/dcp_patcher.dart | 5 +- gui/lib/patcher/patch_manifest.dart | 35 +-- gui/lib/patcher/ups_patcher.dart | 5 +- gui/lib/providers/locale_provider.dart | 10 +- gui/lib/screens/dreamcast_screen.dart | 36 +-- gui/lib/screens/home_screen.dart | 12 +- gui/lib/screens/patcher_screen.dart | 16 +- gui/lib/screens/settings_screen.dart | 47 ++-- gui/lib/screens/theme_editor_screen.dart | 5 +- gui/lib/screens/tools_screen.dart | 94 ++++---- gui/lib/services/core_bridge.dart | 206 +++++++++--------- gui/lib/services/patch_builder_service.dart | 2 +- .../services/retro_achievements_service.dart | 10 +- gui/lib/services/theme_service.dart | 20 +- gui/lib/services/tools_service.dart | 122 ++++++----- gui/lib/services/update_service.dart | 10 +- gui/lib/widgets/rom_card.dart | 14 +- gui/lib/widgets/sidebar.dart | 5 +- gui/lib/widgets/top_bar.dart | 5 +- gui/test/home_controller_test.dart | 27 +-- gui/test/patch_manifest_test.dart | 5 +- gui/test/patcher_builder_test.dart | 10 +- 34 files changed, 462 insertions(+), 426 deletions(-) diff --git a/gui/lib/app.dart b/gui/lib/app.dart index 1c5b665..ed8a0e4 100644 --- a/gui/lib/app.dart +++ b/gui/lib/app.dart @@ -125,14 +125,14 @@ class _OpenROMAppState extends State { localeProvider: widget.localeProvider, onSettingsChanged: (format, compression, verify, outputDir, sameFolder, audioCodec) { - setState(() { - _format = format; - _compression = compression; - _verify = verify; - _outputDir = outputDir; - _audioCodec = audioCodec; - }); - }, + setState(() { + _format = format; + _compression = compression; + _verify = verify; + _outputDir = outputDir; + _audioCodec = audioCodec; + }); + }, ); return MaterialApp( @@ -174,10 +174,7 @@ class _OpenROMAppState extends State { child: IndexedStack( index: _selectedIndex, children: [ - HomeScreen( - theme: theme, - controller: _homeController, - ), + HomeScreen(theme: theme, controller: _homeController), PatcherScreen(theme: theme), ToolsScreen(theme: theme), DreamcastScreen(theme: theme), diff --git a/gui/lib/controllers/home_controller.dart b/gui/lib/controllers/home_controller.dart index 3693a66..4607eb2 100644 --- a/gui/lib/controllers/home_controller.dart +++ b/gui/lib/controllers/home_controller.dart @@ -39,8 +39,9 @@ class HomeController extends ChangeNotifier { final roms = await FileDetector.detectPaths(paths); for (final rom in roms) { if (!_jobs.any((j) => j.romFile.filepath == rom.filepath)) { - final defaultTarget = - rom.validTargets.isNotEmpty ? rom.validTargets.first : 'CHD'; + final defaultTarget = rom.validTargets.isNotEmpty + ? rom.validTargets.first + : 'CHD'; _jobs.add( ConversionJob( id: DateTime.now().microsecondsSinceEpoch.toString(), @@ -58,9 +59,7 @@ class HomeController extends ChangeNotifier { } } - void pickFiles({ - void Function(OpenROMException e)? onError, - }) async { + void pickFiles({void Function(OpenROMException e)? onError}) async { final result = await FilePicker.platform.pickFiles( allowMultiple: true, type: FileType.any, @@ -156,9 +155,7 @@ class HomeController extends ChangeNotifier { job.status = JobStatus.failed; job.error = e.error; job.errorMessage = e.details ?? e.error.message; - _logs.add( - '[ERROR] ${e.error.title}: ${e.details ?? e.error.message}', - ); + _logs.add('[ERROR] ${e.error.title}: ${e.details ?? e.error.message}'); notifyListeners(); if (onError != null) { onError(e); diff --git a/gui/lib/l10n/app_localizations.dart b/gui/lib/l10n/app_localizations.dart index 82243de..7995fdd 100644 --- a/gui/lib/l10n/app_localizations.dart +++ b/gui/lib/l10n/app_localizations.dart @@ -73,7 +73,7 @@ import 'app_localizations_zh_TW.dart'; /// property. abstract class AppLocalizations { AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -96,11 +96,11 @@ abstract class AppLocalizations { /// of delegates is preferred or required. static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ @@ -115,7 +115,7 @@ abstract class AppLocalizations { Locale('ru'), Locale('tr'), Locale('zh'), - Locale('zh_TW') + Locale('zh_TW'), ]; /// No description provided for @appTitle. @@ -1000,18 +1000,18 @@ class _AppLocalizationsDelegate @override bool isSupported(Locale locale) => [ - 'ar', - 'de', - 'en', - 'es', - 'fr', - 'it', - 'ja', - 'pt', - 'ru', - 'tr', - 'zh' - ].contains(locale.languageCode); + 'ar', + 'de', + 'en', + 'es', + 'fr', + 'it', + 'ja', + 'pt', + 'ru', + 'tr', + 'zh', + ].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; @@ -1041,7 +1041,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) { case 'tr': return AppLocalizationsTr(); case 'zh': - if (locale.countryCode == 'TW' || locale.countryCode == 'HK' || locale.countryCode == 'MO') { + if (locale.countryCode == 'TW' || + locale.countryCode == 'HK' || + locale.countryCode == 'MO') { return AppLocalizationsZhTw(); } return AppLocalizationsZh(); @@ -1050,8 +1052,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) { } throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.'); + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); } diff --git a/gui/lib/l10n/app_localizations_zh.dart b/gui/lib/l10n/app_localizations_zh.dart index 5cff621..8d22d2a 100644 --- a/gui/lib/l10n/app_localizations_zh.dart +++ b/gui/lib/l10n/app_localizations_zh.dart @@ -35,7 +35,7 @@ class AppLocalizationsZh extends AppLocalizations { String get outputFormat => '输出格式'; @override - String get compressionLevel => '压缩级别'; + String get compressionLevel => '压缩等级'; @override String get compressionNormal => '标准'; @@ -53,7 +53,7 @@ class AppLocalizationsZh extends AppLocalizations { String get verifyAfterConversion => '转换后验证'; @override - String get outputDestination => '输出目标'; + String get outputDestination => '输出位置'; @override String get browse => '浏览'; @@ -110,7 +110,7 @@ class AppLocalizationsZh extends AppLocalizations { String get errorCorruptedFile => '文件似乎已损坏或为 0 字节。'; @override - String get errorUnknown => '发生错误。这可能是程序缺陷。'; + String get errorUnknown => '发生错误。这可能是 Bug。'; @override String get errorDetailsLabel => '详细信息'; @@ -185,7 +185,7 @@ class AppLocalizationsZh extends AppLocalizations { String get compressorOutputFormat => '输出格式'; @override - String get compressorLevel => '压缩级别'; + String get compressorLevel => '压缩等级'; @override String get compressorLevelFast => '快速'; @@ -465,4 +465,3 @@ class AppLocalizationsZh extends AppLocalizations { @override String get raSaveCredentials => '保存'; } - diff --git a/gui/lib/l10n/app_localizations_zh_TW.dart b/gui/lib/l10n/app_localizations_zh_TW.dart index a114fe9..217dba4 100644 --- a/gui/lib/l10n/app_localizations_zh_TW.dart +++ b/gui/lib/l10n/app_localizations_zh_TW.dart @@ -35,7 +35,7 @@ class AppLocalizationsZhTw extends AppLocalizations { String get outputFormat => '輸出格式'; @override - String get compressionLevel => '壓縮層級'; + String get compressionLevel => '壓縮等級'; @override String get compressionNormal => '標準'; @@ -53,7 +53,7 @@ class AppLocalizationsZhTw extends AppLocalizations { String get verifyAfterConversion => '轉換後驗證'; @override - String get outputDestination => '輸出目標'; + String get outputDestination => '輸出位置'; @override String get browse => '瀏覽'; @@ -83,20 +83,16 @@ class AppLocalizationsZhTw extends AppLocalizations { String get themeScreen => '主題'; @override - String get errorCoreNotFound => - '應用程式資料夾中缺少 openrom-core。'; + String get errorCoreNotFound => '應用程式資料夾中缺少 openrom-core。'; @override - String get errorUnsupportedFormat => - '此檔案格式無法使用選擇的輸出格式進行轉換。'; + String get errorUnsupportedFormat => '此檔案格式無法使用選擇的輸出格式進行轉換。'; @override - String get errorDiskSpace => - '可用空間不足以完成此次轉換。'; + String get errorDiskSpace => '可用空間不足以完成此次轉換。'; @override - String get errorToolFailed => - '轉換工具(chdman/maxcso)未預期結束。'; + String get errorToolFailed => '轉換工具(chdman/maxcso)未預期結束。'; @override String get errorConversionFailed => '轉換在完成前停止。'; @@ -105,19 +101,16 @@ class AppLocalizationsZhTw extends AppLocalizations { String get errorFileNotFound => '輸入檔案已被移動或刪除。'; @override - String get errorOutputDirNotFound => - '選擇的輸出資料夾已不存在。'; + String get errorOutputDirNotFound => '選擇的輸出資料夾已不存在。'; @override - String get errorPermissionDenied => - 'OpenROM 無法寫入輸出資料夾。'; + String get errorPermissionDenied => 'OpenROM 無法寫入輸出資料夾。'; @override - String get errorCorruptedFile => - '檔案似乎已損毀或為 0 bytes。'; + String get errorCorruptedFile => '檔案似乎已損毀或為 0 bytes。'; @override - String get errorUnknown => '發生錯誤。這可能是程式缺陷。'; + String get errorUnknown => '發生錯誤。這可能是 Bug。'; @override String get errorDetailsLabel => '詳細資料'; @@ -192,7 +185,7 @@ class AppLocalizationsZhTw extends AppLocalizations { String get compressorOutputFormat => '輸出格式'; @override - String get compressorLevel => '壓縮層級'; + String get compressorLevel => '壓縮等級'; @override String get compressorLevelFast => '快速'; @@ -408,8 +401,7 @@ class AppLocalizationsZhTw extends AppLocalizations { String get onlineFeaturesSection => '線上功能'; @override - String get onlineFeaturesSub => - '所有功能均為選擇性啟用。未經您同意,不會收集任何資料。'; + String get onlineFeaturesSub => '所有功能均為選擇性啟用。未經您同意,不會收集任何資料。'; @override String get autoUpdateTitle => '自動檢查更新'; diff --git a/gui/lib/l10n/app_zh.arb b/gui/lib/l10n/app_zh.arb index 3f338f4..e72e6bf 100644 --- a/gui/lib/l10n/app_zh.arb +++ b/gui/lib/l10n/app_zh.arb @@ -15,13 +15,13 @@ "dropZoneSubHint": "或点击浏览", "settingsTitle": "转换设置", "outputFormat": "输出格式", - "compressionLevel": "压缩级别", + "compressionLevel": "压缩等级", "compressionNormal": "标准", "compressionHigh": "高", "compressionMax": "最大", "postProcessing": "后处理", "verifyAfterConversion": "转换后验证", - "outputDestination": "输出目标", + "outputDestination": "输出位置", "browse": "浏览", "statusWaiting": "等待中...", "statusConverting": "转换中...", @@ -40,7 +40,7 @@ "errorOutputDirNotFound": "所选输出文件夹已不存在。", "errorPermissionDenied": "OpenROM 无法写入输出文件夹。", "errorCorruptedFile": "文件似乎已损坏或为 0 字节。", - "errorUnknown": "发生错误。这可能是程序缺陷。", + "errorUnknown": "发生错误。这可能是 Bug。", "errorDetailsLabel": "详细信息", "retryButton": "重试", "closeButton": "关闭", @@ -65,7 +65,7 @@ "compressorAddFiles": "添加文件", "compressorAddFolder": "添加文件夹", "compressorOutputFormat": "输出格式", - "compressorLevel": "压缩级别", + "compressorLevel": "压缩等级", "compressorLevelFast": "快速", "compressorLevelNormal": "标准", "compressorLevelUltra": "极致", diff --git a/gui/lib/l10n/app_zh_TW.arb b/gui/lib/l10n/app_zh_TW.arb index 5b6e1f9..e1dd52c 100644 --- a/gui/lib/l10n/app_zh_TW.arb +++ b/gui/lib/l10n/app_zh_TW.arb @@ -15,13 +15,13 @@ "dropZoneSubHint": "或點擊瀏覽", "settingsTitle": "轉換設定", "outputFormat": "輸出格式", - "compressionLevel": "壓縮層級", + "compressionLevel": "壓縮等級", "compressionNormal": "標準", "compressionHigh": "高", "compressionMax": "最高", "postProcessing": "後續處理", "verifyAfterConversion": "轉換後驗證", - "outputDestination": "輸出目標", + "outputDestination": "輸出位置", "browse": "瀏覽", "statusWaiting": "等待中...", "statusConverting": "轉換中...", @@ -40,7 +40,7 @@ "errorOutputDirNotFound": "選擇的輸出資料夾已不存在。", "errorPermissionDenied": "OpenROM 無法寫入輸出資料夾。", "errorCorruptedFile": "檔案似乎已損毀或為 0 bytes。", - "errorUnknown": "發生錯誤。這可能是程式缺陷。", + "errorUnknown": "發生錯誤。這可能是 Bug。", "errorDetailsLabel": "詳細資料", "retryButton": "重試", "closeButton": "關閉", @@ -65,7 +65,7 @@ "compressorAddFiles": "新增檔案", "compressorAddFolder": "新增資料夾", "compressorOutputFormat": "輸出格式", - "compressorLevel": "壓縮層級", + "compressorLevel": "壓縮等級", "compressorLevelFast": "快速", "compressorLevelNormal": "標準", "compressorLevelUltra": "極致", diff --git a/gui/lib/models/conversion_job.dart b/gui/lib/models/conversion_job.dart index 29eef76..74621d6 100644 --- a/gui/lib/models/conversion_job.dart +++ b/gui/lib/models/conversion_job.dart @@ -51,8 +51,8 @@ class ConversionJob { ratio = compression == 'Max' ? 0.60 : compression == 'Fast' - ? 0.80 - : 0.70; + ? 0.80 + : 0.70; break; case 'RVZ': ratio = compression == 'Max' ? 0.50 : 0.55; diff --git a/gui/lib/models/rom_file.dart b/gui/lib/models/rom_file.dart index 6d23f06..8ff4c8d 100644 --- a/gui/lib/models/rom_file.dart +++ b/gui/lib/models/rom_file.dart @@ -35,13 +35,15 @@ class RomFile { factory RomFile.fromJson(Map json) { return RomFile( filepath: json['filepath'] ?? '', - filename: json['filename'] ?? + filename: + json['filename'] ?? (json['filepath'] != null ? json['filepath'].split('/')['last'] : ''), format: json['format'] ?? 'UNKNOWN', platform: json['platform'] ?? 'ROM File', sizeBytes: json['size_bytes'] ?? 0, sizeStr: json['size_str'] ?? '0 B', - validTargets: (json['valid_targets'] as List?) + validTargets: + (json['valid_targets'] as List?) ?.map((e) => e.toString()) .toList() ?? [], diff --git a/gui/lib/patcher/aps_gba_builder.dart b/gui/lib/patcher/aps_gba_builder.dart index 9b35d22..c46979f 100644 --- a/gui/lib/patcher/aps_gba_builder.dart +++ b/gui/lib/patcher/aps_gba_builder.dart @@ -45,14 +45,16 @@ class ApsGbaBuilder { final modChunk = Uint8List(_chunkSize); if (offset < origLen) { - final end = - (offset + _chunkSize) < origLen ? (offset + _chunkSize) : origLen; + final end = (offset + _chunkSize) < origLen + ? (offset + _chunkSize) + : origLen; origChunk.setRange(0, end - offset, origBytes.sublist(offset, end)); } if (offset < modLen) { - final end = - (offset + _chunkSize) < modLen ? (offset + _chunkSize) : modLen; + final end = (offset + _chunkSize) < modLen + ? (offset + _chunkSize) + : modLen; modChunk.setRange(0, end - offset, modBytes.sublist(offset, end)); } diff --git a/gui/lib/patcher/aps_n64_patcher.dart b/gui/lib/patcher/aps_n64_patcher.dart index 9718d68..b32fdb2 100644 --- a/gui/lib/patcher/aps_n64_patcher.dart +++ b/gui/lib/patcher/aps_n64_patcher.dart @@ -67,7 +67,8 @@ class ApsN64Patcher extends RomPatcher { if (patchType == _typeN64Patch) { validatesRomHeader = true; final endianness = await patch.readByte(); - final cardId = ((await patch.readByte() & 0xff) << 8) + + final cardId = + ((await patch.readByte() & 0xff) << 8) + (await patch.readByte() & 0xff); final country = await patch.readByte(); final crc = await patch.read(8); @@ -180,10 +181,12 @@ class ApsN64Patcher extends RomPatcher { // Check cartridge ID. await rom.setPosition(0x3c); if (endianness == 1) { - val = ((await rom.readByte() & 0xff) << 8) + + val = + ((await rom.readByte() & 0xff) << 8) + (await rom.readByte() & 0xff); } else { - val = (await rom.readByte() & 0xff) + + val = + (await rom.readByte() & 0xff) + ((await rom.readByte() & 0xff) << 8); } if (cartId != val) return false; diff --git a/gui/lib/patcher/aps_patcher.dart b/gui/lib/patcher/aps_patcher.dart index 7a7f7bd..e328518 100644 --- a/gui/lib/patcher/aps_patcher.dart +++ b/gui/lib/patcher/aps_patcher.dart @@ -25,15 +25,15 @@ class ApsPatcher extends RomPatcher { Future apply({bool ignoreChecksum = false}) async { final RomPatcher delegate = switch (await _detectType()) { _ApsType.n64 => ApsN64Patcher( - patchFile: patchFile, - romFile: romFile, - outputFile: outputFile, - ), + patchFile: patchFile, + romFile: romFile, + outputFile: outputFile, + ), _ApsType.gba => ApsGbaPatcher( - patchFile: patchFile, - romFile: romFile, - outputFile: outputFile, - ), + patchFile: patchFile, + romFile: romFile, + outputFile: outputFile, + ), _ApsType.unknown => throw PatchException("Not a valid APS patch."), }; return delegate.apply(ignoreChecksum: ignoreChecksum); diff --git a/gui/lib/patcher/dcp_patcher.dart b/gui/lib/patcher/dcp_patcher.dart index 54389f7..5a662c6 100644 --- a/gui/lib/patcher/dcp_patcher.dart +++ b/gui/lib/patcher/dcp_patcher.dart @@ -40,8 +40,9 @@ class DcpPatcher { int filesPatched = 0; bool ipbinReplaced = false; try { - final lines = - LineSplitter.split(result.stdout).where((l) => l.trim().isNotEmpty); + final lines = LineSplitter.split( + result.stdout, + ).where((l) => l.trim().isNotEmpty); for (final line in lines.toList().reversed) { final decoded = jsonDecode(line); if (decoded is Map) { diff --git a/gui/lib/patcher/patch_manifest.dart b/gui/lib/patcher/patch_manifest.dart index 358dd4f..40cf3ff 100644 --- a/gui/lib/patcher/patch_manifest.dart +++ b/gui/lib/patcher/patch_manifest.dart @@ -35,18 +35,18 @@ class RomIdentity { } Map toJson() => { - 'fileName': fileName, - 'sizeBytes': sizeBytes, - 'crc32': crc32, - 'sha256': sha256, - }; + 'fileName': fileName, + 'sizeBytes': sizeBytes, + 'crc32': crc32, + 'sha256': sha256, + }; factory RomIdentity.fromJson(Map json) => RomIdentity( - fileName: json['fileName'] as String? ?? '', - sizeBytes: json['sizeBytes'] as int, - crc32: json['crc32'] as String, - sha256: json['sha256'] as String, - ); + fileName: json['fileName'] as String? ?? '', + sizeBytes: json['sizeBytes'] as int, + crc32: json['crc32'] as String, + sha256: json['sha256'] as String, + ); } /// Sidecar metadata written next to generated patches. @@ -68,11 +68,11 @@ class PatchManifest { }); Map toJson() => { - 'schemaVersion': schemaVersion, - 'format': format, - 'baseRom': baseRom.toJson(), - 'targetRom': targetRom.toJson(), - }; + 'schemaVersion': schemaVersion, + 'format': format, + 'baseRom': baseRom.toJson(), + 'targetRom': targetRom.toJson(), + }; factory PatchManifest.fromJson(Map json) { if (json['schemaVersion'] != schemaVersion) { @@ -81,8 +81,9 @@ class PatchManifest { return PatchManifest( format: json['format'] as String? ?? 'unknown', baseRom: RomIdentity.fromJson(json['baseRom'] as Map), - targetRom: - RomIdentity.fromJson(json['targetRom'] as Map), + targetRom: RomIdentity.fromJson( + json['targetRom'] as Map, + ), ); } diff --git a/gui/lib/patcher/ups_patcher.dart b/gui/lib/patcher/ups_patcher.dart index 7a3c3d5..c492f51 100644 --- a/gui/lib/patcher/ups_patcher.dart +++ b/gui/lib/patcher/ups_patcher.dart @@ -103,8 +103,9 @@ class UpsPatcher extends RomPatcher { final copyLen = offset - outPos; if (copyLen > 0) { final available = romLen - romCursor; - final n = - copyLen <= available ? copyLen : (available > 0 ? available : 0); + final n = copyLen <= available + ? copyLen + : (available > 0 ? available : 0); if (n > 0) { out.setRange(outPos, outPos + n, rom, romCursor); romCursor += n; diff --git a/gui/lib/providers/locale_provider.dart b/gui/lib/providers/locale_provider.dart index d73f494..c044dce 100644 --- a/gui/lib/providers/locale_provider.dart +++ b/gui/lib/providers/locale_provider.dart @@ -44,9 +44,11 @@ class LocaleProvider extends ChangeNotifier { final parts = code.split('_'); if (parts.length == 2) { final locale = Locale(parts[0], parts[1]); - if (supportedLocales.any((l) => - l.languageCode == locale.languageCode && - l.countryCode == locale.countryCode)) { + if (supportedLocales.any( + (l) => + l.languageCode == locale.languageCode && + l.countryCode == locale.countryCode, + )) { _locale = locale; notifyListeners(); } @@ -90,4 +92,4 @@ class LocaleProvider extends ChangeNotifier { return 'English'; } } -} \ No newline at end of file +} diff --git a/gui/lib/screens/dreamcast_screen.dart b/gui/lib/screens/dreamcast_screen.dart index 383e8ab..304d257 100644 --- a/gui/lib/screens/dreamcast_screen.dart +++ b/gui/lib/screens/dreamcast_screen.dart @@ -558,9 +558,9 @@ class _IpBinEditorTabState extends State<_IpBinEditorTab> { ]); if (res.exitCode == 0) { - final lines = LineSplitter.split(res.stdout.toString()) - .where((l) => l.trim().isNotEmpty) - .toList(); + final lines = LineSplitter.split( + res.stdout.toString(), + ).where((l) => l.trim().isNotEmpty).toList(); Map? jsonMap; for (final line in lines.reversed) { @@ -582,9 +582,10 @@ class _IpBinEditorTabState extends State<_IpBinEditorTab> { final regions = (fields['regions'] as List?)?.map((e) => e.toString()).toList() ?? - []; - final peripheralsStr = - (fields['peripherals'] ?? '').toString().trim(); + []; + final peripheralsStr = (fields['peripherals'] ?? '') + .toString() + .trim(); int peripFlags = 0; try { peripFlags = int.parse(peripheralsStr, radix: 16); @@ -592,12 +593,12 @@ class _IpBinEditorTabState extends State<_IpBinEditorTab> { setState(() { _titleController.text = fullTitle; - _productNumberController.text = - (fields['product_number'] ?? '').toString(); - _versionController.text = - (fields['product_version'] ?? '').toString(); - _releaseDateController.text = - (fields['release_date'] ?? '').toString(); + _productNumberController.text = (fields['product_number'] ?? '') + .toString(); + _versionController.text = (fields['product_version'] ?? '') + .toString(); + _releaseDateController.text = (fields['release_date'] ?? '') + .toString(); _japan = regions.contains('Japan'); _usa = regions.contains('USA'); @@ -1066,9 +1067,9 @@ class _GdiInfoTabState extends State<_GdiInfoTab> { final res = await CoreBridge.runCore(['--read-gdi', _gdiPath]); if (res.exitCode == 0) { - final lines = LineSplitter.split(res.stdout.toString()) - .where((l) => l.trim().isNotEmpty) - .toList(); + final lines = LineSplitter.split( + res.stdout.toString(), + ).where((l) => l.trim().isNotEmpty).toList(); List? jsonList; for (final line in lines.reversed) { @@ -1189,8 +1190,9 @@ class _GdiInfoTabState extends State<_GdiInfoTab> { vertical: 14, ), ), - onPressed: - (_gdiPath.isNotEmpty && !_isLoading) ? _loadGdi : null, + onPressed: (_gdiPath.isNotEmpty && !_isLoading) + ? _loadGdi + : null, child: _isLoading ? SizedBox( width: 18, diff --git a/gui/lib/screens/home_screen.dart b/gui/lib/screens/home_screen.dart index f40bb54..ea4aaa2 100644 --- a/gui/lib/screens/home_screen.dart +++ b/gui/lib/screens/home_screen.dart @@ -18,11 +18,7 @@ class HomeScreen extends StatefulWidget { final ThemeConfig theme; final HomeController controller; - const HomeScreen({ - super.key, - required this.theme, - required this.controller, - }); + const HomeScreen({super.key, required this.theme, required this.controller}); @override State createState() => _HomeScreenState(); @@ -83,10 +79,8 @@ class _HomeScreenState extends State { return DropZone( theme: widget.theme, - onFilesDropped: (paths) => controller.addFilesFromPaths( - paths, - onError: _handleError, - ), + onFilesDropped: (paths) => + controller.addFilesFromPaths(paths, onError: _handleError), child: Column( children: [ if (jobs.any( diff --git a/gui/lib/screens/patcher_screen.dart b/gui/lib/screens/patcher_screen.dart index f6e20d5..f0dab81 100644 --- a/gui/lib/screens/patcher_screen.dart +++ b/gui/lib/screens/patcher_screen.dart @@ -266,7 +266,8 @@ class _ApplyPatchTabState extends State<_ApplyPatchTab> { final l10n = AppLocalizations.of(context); final theme = widget.theme; final formatBadge = PatcherFactory.formatName(_patchPath); - final canApply = !_isPatching && + final canApply = + !_isPatching && _romPath.isNotEmpty && _patchPath.isNotEmpty && (_isSspPatch || _outputPath.isNotEmpty); @@ -643,8 +644,9 @@ class _ApplyPatchTabState extends State<_ApplyPatchTab> { child: Text( path.isEmpty ? '...' : path, style: TextStyle( - color: - path.isEmpty ? theme.textSecondary : theme.textPrimary, + color: path.isEmpty + ? theme.textSecondary + : theme.textPrimary, fontSize: 13, ), maxLines: 1, @@ -846,7 +848,8 @@ class _BuildPatchTabState extends State<_BuildPatchTab> { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = widget.theme; - final canBuild = !_isBuilding && + final canBuild = + !_isBuilding && _originalPath.isNotEmpty && _modifiedPath.isNotEmpty && _outputPath.isNotEmpty; @@ -1097,8 +1100,9 @@ class _BuildPatchTabState extends State<_BuildPatchTab> { child: Text( path.isEmpty ? '...' : path, style: TextStyle( - color: - path.isEmpty ? theme.textSecondary : theme.textPrimary, + color: path.isEmpty + ? theme.textSecondary + : theme.textPrimary, fontSize: 13, ), maxLines: 1, diff --git a/gui/lib/screens/settings_screen.dart b/gui/lib/screens/settings_screen.dart index 1ffc0eb..199495a 100644 --- a/gui/lib/screens/settings_screen.dart +++ b/gui/lib/screens/settings_screen.dart @@ -24,7 +24,8 @@ class SettingsScreen extends StatefulWidget { String outputDir, bool sameFolder, String audioCodec, - ) onSettingsChanged; + ) + onSettingsChanged; const SettingsScreen({ super.key, @@ -116,9 +117,13 @@ class _SettingsScreenState extends State { await prefs.setBool(OnlinePrefs.autoUpdateEnabled, _autoUpdateEnabled); await prefs.setBool(OnlinePrefs.raEnabled, _raEnabled); await prefs.setString( - OnlinePrefs.raUsername, _raUsernameController.text.trim()); + OnlinePrefs.raUsername, + _raUsernameController.text.trim(), + ); await prefs.setString( - OnlinePrefs.raApiKey, _raApiKeyController.text.trim()); + OnlinePrefs.raApiKey, + _raApiKeyController.text.trim(), + ); _notifyParent(); } @@ -135,8 +140,8 @@ class _SettingsScreenState extends State { } void _pickOutputDirectory() async { - final String? selectedDirectory = - await FilePicker.platform.getDirectoryPath(); + final String? selectedDirectory = await FilePicker.platform + .getDirectoryPath(); if (selectedDirectory != null) { setState(() { _outputDestination = selectedDirectory; @@ -483,10 +488,7 @@ class _SettingsScreenState extends State { _buildSectionTitle(l10n.onlineFeaturesSection), Text( l10n.onlineFeaturesSub, - style: TextStyle( - color: theme.textSecondary, - fontSize: 13, - ), + style: TextStyle(color: theme.textSecondary, fontSize: 13), ), const SizedBox(height: 16), @@ -532,15 +534,18 @@ class _SettingsScreenState extends State { Text( l10n.autoUpdateUpToDate(_currentVersion), style: const TextStyle( - color: Colors.greenAccent, fontSize: 13), + color: Colors.greenAccent, + fontSize: 13, + ), ) else if (_updateStatus == 'available') ...[ Text( l10n.autoUpdateAvailable(_latestVersion), style: TextStyle( - color: theme.accent, - fontSize: 13, - fontWeight: FontWeight.bold), + color: theme.accent, + fontSize: 13, + fontWeight: FontWeight.bold, + ), ), const SizedBox(width: 8), if (_releaseUrl.isNotEmpty) @@ -549,12 +554,16 @@ class _SettingsScreenState extends State { style: ElevatedButton.styleFrom( backgroundColor: theme.accent, padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 8), + horizontal: 12, + vertical: 8, + ), ), child: Text( l10n.autoUpdateDownload, - style: - const TextStyle(color: Colors.white, fontSize: 12), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), ), ), ] else if (_updateStatus == 'error') @@ -648,8 +657,10 @@ class _SettingsScreenState extends State { _saveSettings(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: - Text('RetroAchievements credentials saved')), + content: Text( + 'RetroAchievements credentials saved', + ), + ), ); }, style: ElevatedButton.styleFrom( diff --git a/gui/lib/screens/theme_editor_screen.dart b/gui/lib/screens/theme_editor_screen.dart index 00925ed..f0d2efe 100644 --- a/gui/lib/screens/theme_editor_screen.dart +++ b/gui/lib/screens/theme_editor_screen.dart @@ -67,8 +67,9 @@ class _ThemeEditorScreenState extends State { 1, (v) { setInner(() { - temp = - HSVColor.fromColor(temp).withSaturation(v).toColor(); + temp = HSVColor.fromColor( + temp, + ).withSaturation(v).toColor(); }); }, temp, diff --git a/gui/lib/screens/tools_screen.dart b/gui/lib/screens/tools_screen.dart index cda599a..52ca979 100644 --- a/gui/lib/screens/tools_screen.dart +++ b/gui/lib/screens/tools_screen.dart @@ -117,7 +117,7 @@ class _ToolsScreenState extends State class _CompressorQueueItem { final String filepath; String - status; // "Queued", "Compressing", "Extracting", "Done", "Failed", "Skipped" + status; // "Queued", "Compressing", "Extracting", "Done", "Failed", "Skipped" double progress; String? error; @@ -399,8 +399,9 @@ class _CompressorTabState extends State<_CompressorTab> { backgroundColor: theme.surface, labelStyle: TextStyle( color: selected ? Colors.black : theme.textPrimary, - fontWeight: - selected ? FontWeight.bold : FontWeight.normal, + fontWeight: selected + ? FontWeight.bold + : FontWeight.normal, ), onSelected: (_) { setState(() { @@ -408,12 +409,14 @@ class _CompressorTabState extends State<_CompressorTab> { for (final item in _queue) { if (item.status == 'Skipped' || item.status == 'Queued') { - final isSkipped = (_format != 'Extract') && + final isSkipped = + (_format != 'Extract') && _isAlreadyCompressed(item.filepath); item.status = isSkipped ? 'Skipped' : 'Queued'; item.progress = isSkipped ? 100.0 : 0.0; - item.error = - isSkipped ? l10n.compressorSkipped : null; + item.error = isSkipped + ? l10n.compressorSkipped + : null; } } }); @@ -436,30 +439,32 @@ class _CompressorTabState extends State<_CompressorTab> { ), const SizedBox(height: 8), Row( - children: [ - MapEntry('Fast', l10n.compressorLevelFast), - MapEntry('Normal', l10n.compressorLevelNormal), - MapEntry('Ultra', l10n.compressorLevelUltra), - ].map((entry) { - final key = entry.key; - final label = entry.value; - final selected = _level == key; - return Padding( - padding: const EdgeInsets.only(right: 10), - child: ChoiceChip( - label: Text(label), - selected: selected, - selectedColor: theme.accent, - backgroundColor: theme.surface, - labelStyle: TextStyle( - color: selected ? Colors.black : theme.textPrimary, - fontWeight: - selected ? FontWeight.bold : FontWeight.normal, - ), - onSelected: (_) => setState(() => _level = key), - ), - ); - }).toList(), + children: + [ + MapEntry('Fast', l10n.compressorLevelFast), + MapEntry('Normal', l10n.compressorLevelNormal), + MapEntry('Ultra', l10n.compressorLevelUltra), + ].map((entry) { + final key = entry.key; + final label = entry.value; + final selected = _level == key; + return Padding( + padding: const EdgeInsets.only(right: 10), + child: ChoiceChip( + label: Text(label), + selected: selected, + selectedColor: theme.accent, + backgroundColor: theme.surface, + labelStyle: TextStyle( + color: selected ? Colors.black : theme.textPrimary, + fontWeight: selected + ? FontWeight.bold + : FontWeight.normal, + ), + onSelected: (_) => setState(() => _level = key), + ), + ); + }).toList(), ), const SizedBox(height: 16), ], @@ -1612,8 +1617,9 @@ class _BinMergerTabState extends State<_BinMergerTab> { width: double.infinity, height: 46, child: ElevatedButton( - onPressed: - (_cuePath.isNotEmpty && !_isProcessing) ? _mergeBins : null, + onPressed: (_cuePath.isNotEmpty && !_isProcessing) + ? _mergeBins + : null, style: ElevatedButton.styleFrom( backgroundColor: theme.accent, foregroundColor: Colors.black, @@ -2132,8 +2138,9 @@ class _CueEditorTabState extends State<_CueEditorTab> { decoration: InputDecoration( contentPadding: const EdgeInsets.all(12), border: InputBorder.none, - hintText: - _cuePath == null ? 'Open a .cue file to edit...' : null, + hintText: _cuePath == null + ? 'Open a .cue file to edit...' + : null, hintStyle: TextStyle(color: widget.theme.textSecondary), ), ), @@ -2371,8 +2378,8 @@ class _RomRenamerTabState extends State<_RomRenamerTab> { ElevatedButton.icon( onPressed: (_isRenaming || _scanResults.isEmpty || _romFolder == null) - ? null - : _rename, + ? null + : _rename, icon: _isRenaming ? const SizedBox( width: 14, @@ -2478,8 +2485,9 @@ class _OneGOneRCleanerTabState extends State<_OneGOneRCleanerTab> { if (_romFolder == null) return; setState(() { _isProcessing = true; - _statusMsg = - _dryRun ? 'Simulating 1G1R filter...' : 'Cleaning duplicates...'; + _statusMsg = _dryRun + ? 'Simulating 1G1R filter...' + : 'Cleaning duplicates...'; _results = null; }); try { @@ -2698,8 +2706,9 @@ class _OneGOneRCleanerTabState extends State<_OneGOneRCleanerTab> { isWinner ? Icons.check_circle : Icons.drive_file_move_outlined, - color: - isWinner ? Colors.greenAccent : Colors.orangeAccent, + color: isWinner + ? Colors.greenAccent + : Colors.orangeAccent, size: 18, ), title: Text( @@ -2707,8 +2716,9 @@ class _OneGOneRCleanerTabState extends State<_OneGOneRCleanerTab> { style: TextStyle( fontSize: 13, color: theme.textPrimary, - fontWeight: - isWinner ? FontWeight.bold : FontWeight.normal, + fontWeight: isWinner + ? FontWeight.bold + : FontWeight.normal, ), ), subtitle: Text( diff --git a/gui/lib/services/core_bridge.dart b/gui/lib/services/core_bridge.dart index e57ef20..8cb781c 100644 --- a/gui/lib/services/core_bridge.dart +++ b/gui/lib/services/core_bridge.dart @@ -33,8 +33,9 @@ class CoreBridge { } final String exeDir = File(Platform.resolvedExecutable).parent.path; - final String binaryName = - Platform.isWindows ? 'openrom-core.exe' : 'openrom-core'; + final String binaryName = Platform.isWindows + ? 'openrom-core.exe' + : 'openrom-core'; final String sameDirBinary = '$exeDir${Platform.pathSeparator}$binaryName'; if (File(sameDirBinary).existsSync()) { @@ -87,9 +88,9 @@ class CoreBridge { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split(result.stdout.toString()) - .where((l) => l.trim().isNotEmpty) - .toList(); + final lines = LineSplitter.split( + result.stdout.toString(), + ).where((l) => l.trim().isNotEmpty).toList(); if (lines.isNotEmpty) { final Map jsonMap = jsonDecode(lines.last); return RomFile.fromJson(jsonMap); @@ -106,7 +107,8 @@ class CoreBridge { ); } } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -189,39 +191,39 @@ class CoreBridge { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - final type = event['type']; - if (type == 'progress') { - final double pct = (event['percent'] as num).toDouble(); - onProgress(pct); - } else if (type == 'log') { - final String msg = event['message'] ?? ''; - onLog(msg); - } else if (type == 'done') { - final bool success = event['success'] ?? false; - final String? err = event['error']; - onDone(success, err); - } else if (type == 'error') { - final String msg = event['message'] ?? 'Unknown error'; - onLog('[ERROR] $msg'); - stderrLog.writeln(msg); - } - } catch (_) { - onLog(line); - } - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + final type = event['type']; + if (type == 'progress') { + final double pct = (event['percent'] as num).toDouble(); + onProgress(pct); + } else if (type == 'log') { + final String msg = event['message'] ?? ''; + onLog(msg); + } else if (type == 'done') { + final bool success = event['success'] ?? false; + final String? err = event['error']; + onDone(success, err); + } else if (type == 'error') { + final String msg = event['message'] ?? 'Unknown error'; + onLog('[ERROR] $msg'); + stderrLog.writeln(msg); + } + } catch (_) { + onLog(line); + } + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - onLog('[STDERR] $line'); - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + onLog('[STDERR] $line'); + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0) { @@ -244,7 +246,8 @@ class CoreBridge { } } } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -308,39 +311,39 @@ class CoreBridge { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - final type = event['type']; - if (type == 'progress') { - final double pct = (event['percent'] as num).toDouble(); - onProgress?.call(pct); - } else if (type == 'log') { - final String msg = event['message'] ?? ''; - onLog?.call(msg); - } else if (type == 'done') { - final bool success = event['success'] ?? false; - final String? err = event['error']; - onDone?.call(success, err); - } else if (type == 'error') { - final String msg = event['message'] ?? 'Unknown error'; - onLog?.call('[ERROR] $msg'); - stderrLog.writeln(msg); - } - } catch (_) { - onLog?.call(line); - } - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + final type = event['type']; + if (type == 'progress') { + final double pct = (event['percent'] as num).toDouble(); + onProgress?.call(pct); + } else if (type == 'log') { + final String msg = event['message'] ?? ''; + onLog?.call(msg); + } else if (type == 'done') { + final bool success = event['success'] ?? false; + final String? err = event['error']; + onDone?.call(success, err); + } else if (type == 'error') { + final String msg = event['message'] ?? 'Unknown error'; + onLog?.call('[ERROR] $msg'); + stderrLog.writeln(msg); + } + } catch (_) { + onLog?.call(line); + } + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - onLog?.call('[STDERR] $line'); - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + onLog?.call('[STDERR] $line'); + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0) { @@ -351,7 +354,8 @@ class CoreBridge { throw OpenROMException(OpenROMError.conversionFailed, details: details); } } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -405,39 +409,39 @@ class CoreBridge { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - final type = event['type']; - if (type == 'progress') { - final double pct = (event['percent'] as num).toDouble(); - onProgress?.call(pct); - } else if (type == 'log') { - final String msg = event['message'] ?? ''; - onLog?.call(msg); - } else if (type == 'done') { - final bool success = event['success'] ?? false; - final String? err = event['error']; - onDone?.call(success, err); - } else if (type == 'error') { - final String msg = event['message'] ?? 'Unknown error'; - onLog?.call('[ERROR] $msg'); - stderrLog.writeln(msg); - } - } catch (_) { - onLog?.call(line); - } - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + final type = event['type']; + if (type == 'progress') { + final double pct = (event['percent'] as num).toDouble(); + onProgress?.call(pct); + } else if (type == 'log') { + final String msg = event['message'] ?? ''; + onLog?.call(msg); + } else if (type == 'done') { + final bool success = event['success'] ?? false; + final String? err = event['error']; + onDone?.call(success, err); + } else if (type == 'error') { + final String msg = event['message'] ?? 'Unknown error'; + onLog?.call('[ERROR] $msg'); + stderrLog.writeln(msg); + } + } catch (_) { + onLog?.call(line); + } + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - onLog?.call('[STDERR] $line'); - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + onLog?.call('[STDERR] $line'); + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0) { @@ -448,7 +452,8 @@ class CoreBridge { throw OpenROMException(OpenROMError.conversionFailed, details: details); } } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -489,9 +494,9 @@ class CoreBridge { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split(result.stdout.toString()) - .where((l) => l.trim().isNotEmpty) - .toList(); + final lines = LineSplitter.split( + result.stdout.toString(), + ).where((l) => l.trim().isNotEmpty).toList(); for (final line in lines.reversed) { try { final Map event = jsonDecode(line); @@ -507,7 +512,8 @@ class CoreBridge { details: err.isNotEmpty ? err : 'M3U generation failed', ); } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -532,9 +538,9 @@ class CoreBridge { } final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split(result.stdout.toString()) - .where((l) => l.trim().isNotEmpty) - .toList(); + final lines = LineSplitter.split( + result.stdout.toString(), + ).where((l) => l.trim().isNotEmpty).toList(); if (lines.isNotEmpty) { final Map jsonMap = jsonDecode(lines.last); final p = jsonMap['path'] as String?; diff --git a/gui/lib/services/patch_builder_service.dart b/gui/lib/services/patch_builder_service.dart index 8f4aecc..1a3c034 100644 --- a/gui/lib/services/patch_builder_service.dart +++ b/gui/lib/services/patch_builder_service.dart @@ -193,7 +193,7 @@ class PatchBuilderService { required String modifiedPath, required String outputPath, required String - format, // 'xdelta' | 'ips' | 'ips32' | 'bps' | 'ups' | 'ebp' | 'ppf3' | 'aps (gba)' | 'aps (n64)' | 'dcp' + format, // 'xdelta' | 'ips' | 'ips32' | 'bps' | 'ups' | 'ebp' | 'ppf3' | 'aps (gba)' | 'aps (n64)' | 'dcp' }) async { try { return await compute(_buildPatchIsolate, { diff --git a/gui/lib/services/retro_achievements_service.dart b/gui/lib/services/retro_achievements_service.dart index cf50dea..61c4216 100644 --- a/gui/lib/services/retro_achievements_service.dart +++ b/gui/lib/services/retro_achievements_service.dart @@ -55,13 +55,9 @@ class RetroAchievementsService { final hash = await computeMd5(filePath); try { - final uri = Uri.parse('$_baseUrl/API_GetGameInfoByMD5.php').replace( - queryParameters: { - 'z': username, - 'y': apiKey, - 'm': hash, - }, - ); + final uri = Uri.parse( + '$_baseUrl/API_GetGameInfoByMD5.php', + ).replace(queryParameters: {'z': username, 'y': apiKey, 'm': hash}); final response = await http.get(uri).timeout(_timeout); diff --git a/gui/lib/services/theme_service.dart b/gui/lib/services/theme_service.dart index 55694ea..ad8db06 100644 --- a/gui/lib/services/theme_service.dart +++ b/gui/lib/services/theme_service.dart @@ -57,12 +57,14 @@ class ThemeService extends ChangeNotifier { // Load user-saved custom themes from disk try { final dir = await getApplicationSupportDirectory(); - final customFiles = - Directory(dir.path).listSync().whereType().where( - (f) => - f.path.endsWith('.json') && - f.uri.pathSegments.last.startsWith('custom_'), - ); + final customFiles = Directory(dir.path) + .listSync() + .whereType() + .where( + (f) => + f.path.endsWith('.json') && + f.uri.pathSegments.last.startsWith('custom_'), + ); for (final file in customFiles) { try { final content = await file.readAsString(); @@ -93,9 +95,9 @@ class ThemeService extends ChangeNotifier { try { final dir = await getApplicationSupportDirectory(); final safeName = theme.name.toLowerCase().replaceAll( - RegExp(r'[^a-z0-9_]'), - '_', - ); + RegExp(r'[^a-z0-9_]'), + '_', + ); final file = File('${dir.path}/custom_$safeName.json'); await file.writeAsString(jsonEncode(theme.toJson())); diff --git a/gui/lib/services/tools_service.dart b/gui/lib/services/tools_service.dart index 44d3ab2..b400559 100644 --- a/gui/lib/services/tools_service.dart +++ b/gui/lib/services/tools_service.dart @@ -102,7 +102,8 @@ class ToolsService { details: err.isNotEmpty ? err : 'CUE generation failed', ); } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -152,33 +153,33 @@ class ToolsService { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - final type = event['type']; - if (type == 'progress') { - final double pct = (event['percent'] as num).toDouble(); - onProgress?.call(pct); - } else if (type == 'done' && event['success'] == true) { - mergeResult = BinMergeResult( - mergedBinPath: event['merged_bin'] as String, - mergedCuePath: event['merged_cue'] as String, - tracksCount: (event['tracks_count'] as num?)?.toInt() ?? 0, - ); - } else if (type == 'error') { - stderrLog.writeln(event['message'] ?? ''); - } - } catch (_) {} - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + final type = event['type']; + if (type == 'progress') { + final double pct = (event['percent'] as num).toDouble(); + onProgress?.call(pct); + } else if (type == 'done' && event['success'] == true) { + mergeResult = BinMergeResult( + mergedBinPath: event['merged_bin'] as String, + mergedCuePath: event['merged_cue'] as String, + tracksCount: (event['tracks_count'] as num?)?.toInt() ?? 0, + ); + } else if (type == 'error') { + stderrLog.writeln(event['message'] ?? ''); + } + } catch (_) {} + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode == 0 && mergeResult != null) { @@ -190,7 +191,8 @@ class ToolsService { : 'Process exited with code $exitCode'; throw OpenROMException(OpenROMError.conversionFailed, details: details); } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -240,7 +242,8 @@ class ToolsService { } return null; } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -306,7 +309,8 @@ class ToolsService { details: err.isNotEmpty ? err : 'Header removal failed', ); } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -422,30 +426,30 @@ class ToolsService { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - if (event['type'] == 'result') { - results.add(Map.from(event)); - } else if (event['type'] == 'progress' && onProgress != null) { - onProgress( - event['file'] as String? ?? '', - (event['percent'] as num?)?.toDouble() ?? 0, - ); - } else if (event['type'] == 'error') { - stderrLog.writeln(event['message'] ?? ''); - } - } catch (_) {} - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + if (event['type'] == 'result') { + results.add(Map.from(event)); + } else if (event['type'] == 'progress' && onProgress != null) { + onProgress( + event['file'] as String? ?? '', + (event['percent'] as num?)?.toDouble() ?? 0, + ); + } else if (event['type'] == 'error') { + stderrLog.writeln(event['message'] ?? ''); + } + } catch (_) {} + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0 && results.isEmpty) { @@ -483,25 +487,25 @@ class ToolsService { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - if (event['type'] == 'rename') { - results.add(Map.from(event)); - } else if (event['type'] == 'error') { - stderrLog.writeln(event['message'] ?? ''); - } - } catch (_) {} - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + if (event['type'] == 'rename') { + results.add(Map.from(event)); + } else if (event['type'] == 'error') { + stderrLog.writeln(event['message'] ?? ''); + } + } catch (_) {} + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0 && results.isEmpty) { diff --git a/gui/lib/services/update_service.dart b/gui/lib/services/update_service.dart index f8c2211..c238e50 100644 --- a/gui/lib/services/update_service.dart +++ b/gui/lib/services/update_service.dart @@ -30,10 +30,12 @@ class UpdateService { /// [currentVersion] should be like "3.7.0" (no "v" prefix). static Future checkForUpdate(String currentVersion) async { try { - final response = await http.get( - Uri.parse(_repoApi), - headers: {'Accept': 'application/vnd.github+json'}, - ).timeout(_timeout); + final response = await http + .get( + Uri.parse(_repoApi), + headers: {'Accept': 'application/vnd.github+json'}, + ) + .timeout(_timeout); if (response.statusCode != 200) { return UpdateCheckResult( diff --git a/gui/lib/widgets/rom_card.dart b/gui/lib/widgets/rom_card.dart index cc6eb76..f073777 100644 --- a/gui/lib/widgets/rom_card.dart +++ b/gui/lib/widgets/rom_card.dart @@ -53,8 +53,10 @@ class _RomCardState extends State { _raChecking = true; }); - final service = - RetroAchievementsService(username: username, apiKey: apiKey); + final service = RetroAchievementsService( + username: username, + apiKey: apiKey, + ); final result = await service.lookupHash(widget.job.romFile.filepath); if (mounted) { @@ -130,8 +132,8 @@ class _RomCardState extends State { final err = widget.job.error ?? OpenROMError.conversionFailed; final details = (widget.job.errorMessage != null && widget.job.errorMessage!.isNotEmpty) - ? widget.job.errorMessage - : widget.job.logs.join('\n'); + ? widget.job.errorMessage + : widget.job.logs.join('\n'); showOpenROMError(context, err, details: details); } @@ -276,8 +278,8 @@ class _RomCardState extends State { color: isFailed ? Colors.redAccent : (widget.job.status == JobStatus.done - ? Colors.greenAccent - : widget.theme.textSecondary), + ? Colors.greenAccent + : widget.theme.textSecondary), fontSize: 13, ), ), diff --git a/gui/lib/widgets/sidebar.dart b/gui/lib/widgets/sidebar.dart index e7c28dc..ddea03d 100644 --- a/gui/lib/widgets/sidebar.dart +++ b/gui/lib/widgets/sidebar.dart @@ -63,8 +63,9 @@ class Sidebar extends StatelessWidget { decoration: BoxDecoration( border: Border( left: BorderSide( - color: - isSelected ? theme.accent : Colors.transparent, + color: isSelected + ? theme.accent + : Colors.transparent, width: 3, ), ), diff --git a/gui/lib/widgets/top_bar.dart b/gui/lib/widgets/top_bar.dart index a938b72..75e1a11 100644 --- a/gui/lib/widgets/top_bar.dart +++ b/gui/lib/widgets/top_bar.dart @@ -70,8 +70,9 @@ class TopBar extends StatelessWidget { ), const SizedBox(width: 12), ElevatedButton( - onPressed: - (fileCount > 0 && !isConverting) ? onConvertPressed : null, + onPressed: (fileCount > 0 && !isConverting) + ? onConvertPressed + : null, style: ElevatedButton.styleFrom( backgroundColor: theme.accent, foregroundColor: Colors.white, diff --git a/gui/test/home_controller_test.dart b/gui/test/home_controller_test.dart index e1ccbe3..2b60425 100644 --- a/gui/test/home_controller_test.dart +++ b/gui/test/home_controller_test.dart @@ -23,17 +23,18 @@ void main() { expect(controller.showTerminal, isFalse); }); test( - 'HomeController prompts for media type on UNKNOWN platform CHD conversion', - () async { - final controller = HomeController(); - bool prompted = false; - controller.startConversion( - onPromptMedia: (j) async { - prompted = true; - return 'cd'; - }, - ); - expect(controller.isConverting, isFalse); - expect(prompted, isFalse); - }); + 'HomeController prompts for media type on UNKNOWN platform CHD conversion', + () async { + final controller = HomeController(); + bool prompted = false; + controller.startConversion( + onPromptMedia: (j) async { + prompted = true; + return 'cd'; + }, + ); + expect(controller.isConverting, isFalse); + expect(prompted, isFalse); + }, + ); } diff --git a/gui/test/patch_manifest_test.dart b/gui/test/patch_manifest_test.dart index 1cc195e..8b7b806 100644 --- a/gui/test/patch_manifest_test.dart +++ b/gui/test/patch_manifest_test.dart @@ -56,9 +56,6 @@ void main() { await manifest.writeForPatch(patch); final loaded = await PatchManifest.readForPatch(patch); - expect( - () => loaded!.validateBase(wrong), - throwsA(isA()), - ); + expect(() => loaded!.validateBase(wrong), throwsA(isA())); }); } diff --git a/gui/test/patcher_builder_test.dart b/gui/test/patcher_builder_test.dart index c9f4f76..6f1d39b 100644 --- a/gui/test/patcher_builder_test.dart +++ b/gui/test/patcher_builder_test.dart @@ -494,14 +494,16 @@ void main() { File(p.join(origDir.path, 'bootsector', 'IP.BIN')) ..createSync(recursive: true) ..writeAsStringSync('ORIG_IPBIN_123'); - File(p.join(origDir.path, '1ST_READ.BIN')) - .writeAsStringSync('ORIG_1ST_READ'); + File( + p.join(origDir.path, '1ST_READ.BIN'), + ).writeAsStringSync('ORIG_1ST_READ'); File(p.join(modDir.path, 'bootsector', 'IP.BIN')) ..createSync(recursive: true) ..writeAsStringSync('MOD_IPBIN_456'); - File(p.join(modDir.path, '1ST_READ.BIN')) - .writeAsStringSync('MOD_1ST_READ'); + File( + p.join(modDir.path, '1ST_READ.BIN'), + ).writeAsStringSync('MOD_1ST_READ'); final builder = DcpBuilder( originalDir: origDir,