diff --git a/App.axaml.cs b/App.axaml.cs index 58488bf..9a88fb4 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -39,7 +39,6 @@ public static IServiceCollection AddServices(this IServiceCollection services) = services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton(); public static IServiceCollection AddViewModels(this IServiceCollection services) => diff --git a/Assets/Langs/en-US.json b/Assets/Langs/en-US.json index dd22352..a74b162 100644 --- a/Assets/Langs/en-US.json +++ b/Assets/Langs/en-US.json @@ -1,12 +1,12 @@ { // Navigation - "modList": "Mod List", + "modList": "MOD List", "settings": "Settings", // Mod List "dragToReorder": "Drag here to reorder. ", "priority": "Priority", - "priorityTooltip": "The higher the priority (lower number), the more important the mod is, and it will overwrite others in conflicts. ", + "priorityTooltip": "The higher the priority (lower number), the more important the MOD is, and it will overwrite others in conflicts. ", "preview": "Preview", "previewTooltip": "Click to view a larger image. ", "id": "ID", @@ -24,4 +24,20 @@ "language": "Language", "selectLanguage": "Select Language", "gameVersionUnsupported": "Selected game version is not supported, please ensure your game version is up-to-date", + + // Dialogs + "ok": "OK", + "cancel": "Cancel", + "conflictsTitle": "Conflicts Detected", + "conflictsContent1": "Found conflicts between: ", + "conflictsContent2": "Click 'OK' if the order is correct, or 'Cancel' to reorder or disable some", + "gameNotFound": "Please locate game in Settings first", + "modInstallSuccess": "MOD installed successfully", + "noDataDirInMod": "No {0} folder found in {1}, seems not a valid MOD", + "noDataDirInGame": "Could not found a data folder in game's root directory, please make sure the game path is correct", + "noEnabledMods": "No MODs have been enabled yet", + "permDeny": "Permission denied when trying to access file: ", + "copyError": "An error occurred while copying the file: ", + "invalidDataI": "Invalid data.i file: ", + "errGenDataI": "An error occurred while generating data.i file: ", } \ No newline at end of file diff --git a/Assets/Langs/ja-JP.json b/Assets/Langs/ja-JP.json index 05ef673..5755265 100644 --- a/Assets/Langs/ja-JP.json +++ b/Assets/Langs/ja-JP.json @@ -1,6 +1,6 @@ { // メニュー - "modList": "Mod リスト", + "modList": "MOD リスト", "settings": "設定", // Mod リスト @@ -10,18 +10,34 @@ "preview": "プレビュー", "previewTooltip": "クリックすると大きな画像が表示されます。", "id":"ID", - "idTooltip": "Mods フォルダ内のフォルダ名。", + "idTooltip": "MOD フォルダ内のフォルダ名。", "name":"名前", "nameTooltip": "クリックして編集してください。", "enabled": "有効", - "openModsFolder": "Mods フォルダを開く。", + "openModsFolder": "MOD フォルダを開く。", "reload": "リロード", - "modIt": "Mod It", + "modIt": "Mod する", // 設定 - "gamePath": "ゲーム パス", - "locateGame": "ゲームを探す", + "gamePath": "ゲームパス", + "locateGame": "ゲームを選ぶ", "language": "言語", "selectLanguage": "言語を選択", - "gameVersionUnsupported": "ゲーム バージョンがサポートされていません,最新のバージョンを使用してください。" + "gameVersionUnsupported": "ゲーム バージョンがサポートされていません,最新のバージョンを使用してください。", + + // Dialogs + "ok": "OK", + "cancel": "キャンセル", + "conflictsTitle": "競合が検出されました", + "conflictsContent1": "以下の Mod 間で競合が検出されました", + "conflictsContent2": "MOD の順序が正しいことを確認したら、「OK」 をクリックして続行し、MOD リストを調整する必要がある場合は、「キャンセル」 をクリックして順序を変更するか、MOD を無効にします。", + "gameNotFound": "まず設定からゲームを選択してください", + "modInstallSuccess": "MODのインストールに成功しました", + "noDataDirInMod": "{1} に {0} フォルダが見つかりません、有効な MOD ではないようです", + "noDataDirInGame": "ゲームのルートディレクトリに data フォルダが見つかりません。ゲームのパスが正しいことを確認してください。", + "noEnabledMods": "MOD はまだ有効になっていません", + "permDeny": "ファイルのアクセス権限がありません: ", + "copyError": "ファイルのコピー中にエラーが発生しました:", + "invalidDataI": "無効な data.i ファイル: ", + "errGenDataI": "data.i ファイルの生成中にエラーが発生しました: ", } \ No newline at end of file diff --git a/Assets/Langs/zh-CN.json b/Assets/Langs/zh-CN.json index 99d72c7..5009a30 100644 --- a/Assets/Langs/zh-CN.json +++ b/Assets/Langs/zh-CN.json @@ -1,27 +1,43 @@ { // 导航菜单 - "modList": "Mod 列表", + "modList": "MOD 列表", "settings": "设置", // Mod 列表 "dragToReorder": "拖拽此处可重新排序", "priority": "优先级", - "priorityTooltip": "优先级越高则 Mod 越重要, 当发生冲突时会覆盖掉优先级低的 Mod", + "priorityTooltip": "优先级越高则 MOD 越重要, 当发生冲突时会覆盖掉优先级低的 MOD", "preview": "预览", "previewTooltip": "点击查看大图", "id": "ID", - "idTooltip": "Mods 文件夹中的每个 Mod 的文件夹名", + "idTooltip": "MOD 文件夹中的每个 MOD 的文件夹名", "name": "名称", "nameTooltip": "点击可以编辑", "enabled": "启用", - "openModsFolder": "打开 Mod 文件夹", + "openModsFolder": "打开 MOD 文件夹", "reload": "刷新", - "modIt": "打 Mod", + "modIt": "打 MOD", // 设置 "gamePath": "游戏路径", "locateGame": "选择游戏", "language": "语言", "selectLanguage": "选择语言", - "gameVersionUnsupported": "该版本的游戏不受支持, 请确保游戏本体已更新至最新" + "gameVersionUnsupported": "该版本的游戏不受支持, 请确保游戏本体已更新至最新", + + // 弹窗 + "ok": "确定", + "cancel": "取消", + "conflictsTitle": "发现冲突", + "conflictsContent1": "在以下 MOD 间发现冲突: ", + "conflictsContent2": "若确认 MOD 顺序正确, 点击 '确定' 以继续; 若需调整 MOD 列表, 点击 '取消' 并重新排序或禁用 MOD", + "gameNotFound": "请先在设置界面选择游戏路径", + "modInstallSuccess": "MOD 安装成功", + "noDataDirInMod": "未能在 {1} 中找到 {0} 文件夹, 看起来它不是一个有效的 MOD", + "noDataDirInGame": "未能在游戏根目录下找到 data 文件夹, 请确认游戏路径是否正确", + "noEnabledMods": "尚未启用任何 MOD", + "permDeny": "尝试访问文件时权限不足: ", + "copyError": "复制文件时发生错误: ", + "invalidDataI": "无效的 data.i 文件: ", + "errGenDataI": "生成 data.i 文件时发生错误: ", } \ No newline at end of file diff --git a/Helpers/MessageBoxHelpers.cs b/Helpers/MessageBoxHelpers.cs new file mode 100644 index 0000000..e2067f4 --- /dev/null +++ b/Helpers/MessageBoxHelpers.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using MsBox.Avalonia; + +namespace RelinkModOrganizer.Helpers; + +public static class MessageBoxHelpers +{ + public static async Task InfoAsync(string message) + { + var msgBox = MessageBoxManager.GetMessageBoxStandard( + string.Empty, + message, + MsBox.Avalonia.Enums.ButtonEnum.Ok, + MsBox.Avalonia.Enums.Icon.Info, + WindowStartupLocation.CenterScreen); + await msgBox.ShowAsync(); + } + + public static async Task ErrorAsync(string message) + { + var msgBox = MessageBoxManager.GetMessageBoxStandard( + string.Empty, + message, + MsBox.Avalonia.Enums.ButtonEnum.Ok, + MsBox.Avalonia.Enums.Icon.Error, + WindowStartupLocation.CenterScreen); + await msgBox.ShowAsync(); + } +} \ No newline at end of file diff --git a/RelinkModOrganizer.csproj b/RelinkModOrganizer.csproj index 9b9ced6..0051c27 100644 --- a/RelinkModOrganizer.csproj +++ b/RelinkModOrganizer.csproj @@ -39,6 +39,7 @@ + diff --git a/Services/ModificationService.cs b/Services/ModificationService.cs index 0c05de1..bd4be4c 100644 --- a/Services/ModificationService.cs +++ b/Services/ModificationService.cs @@ -4,31 +4,40 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using DynamicData.Binding; using ReactiveUI; using RelinkModOrganizer.Models; using RelinkModOrganizer.ThirdParties.DataTools; -using RelinkModOrganizer.ViewModels; using Results = RelinkModOrganizer.TryResults; namespace RelinkModOrganizer.Services; public class ModificationService( ConfigurationService configService, - DataToolsService dataToolsService) + DataToolsService dataToolsService, + LocalizationService localizationService) { public async Task LoadModsFromDiskAsync() { var modsDirPath = Path.Combine(AppContext.BaseDirectory, Consts.ModsDirName); var modIds = new List(); - foreach (var modPath in Directory.EnumerateFileSystemEntries(modsDirPath, "*", SearchOption.TopDirectoryOnly)) + foreach (var modPath in Directory.EnumerateFileSystemEntries( + modsDirPath, + "*", + SearchOption.TopDirectoryOnly)) { var dataDir = Directory .EnumerateDirectories(modPath, Consts.GameDataDirName, SearchOption.AllDirectories) .FirstOrDefault(); + if (string.IsNullOrWhiteSpace(dataDir)) - return Results.Error($"No {Consts.GameDataDirName} folder found in {modPath}, seems not a valid mod"); + { + var msg = string.Format( + localizationService.LocalizedStrings["noDataDirInMod"], + Consts.GameDataDirName, + modPath); + return Results.Error(msg); + } var modId = Path.GetFileName(modPath); var relativeFilePaths = Directory @@ -92,7 +101,7 @@ public TryResult TryCleanUpGameDir() { var gameDirPath = configService.Config.GameDirPath; if (string.IsNullOrWhiteSpace(gameDirPath)) - return Results.Error("Game directory not set"); + return Results.Error(localizationService.LocalizedStrings["gameNotFound"]); var dataDirPath = Path.Combine(gameDirPath, Consts.GameDataDirName); try @@ -116,15 +125,15 @@ public async Task TryCopyModsAsync() { var gameDirPath = configService.Config.GameDirPath; if (string.IsNullOrWhiteSpace(gameDirPath)) - return Results.Error("Game directory not set"); + return Results.Error(localizationService.LocalizedStrings["gameNotFound"]); var gameDataPath = Path.Combine(gameDirPath, Consts.GameDataDirName); if (!Directory.Exists(gameDataPath)) - return Results.Error("Could not found a data directory in game directory, please make sure the game directory is correct"); + return Results.Error(localizationService.LocalizedStrings["noDataDirInGame"]); var enabledMods = configService.Config.Mods.Values.Where(mod => mod.Enabled); if (!enabledMods.Any()) - return Results.Error("No enabled mods found"); + return Results.Error(localizationService.LocalizedStrings["noEnabledMods"]); // Make sure bigger order mods are copied first, then smaller order mods can overwrite them enabledMods = enabledMods.OrderByDescending(mod => mod.Order); @@ -143,7 +152,13 @@ public async Task TryCopyModsAsync() .EnumerateDirectories(srcModPath, Consts.GameDataDirName, SearchOption.AllDirectories) .FirstOrDefault(); if (dataDirPath == null) - return Results.Error($"No '{Consts.GameDataDirName}' folder found in {srcModPath}, seems not a valid mod"); + { + var msg = string.Format( + localizationService.LocalizedStrings["noDataDirInMod"], + Consts.GameDataDirName, + srcModPath); + return Results.Error(msg); + } foreach (var filePath in mod.RelativeFilePaths) { @@ -160,11 +175,11 @@ public async Task TryCopyModsAsync() } catch (UnauthorizedAccessException ex) { - return Results.Error($"Permission denied when trying to access file: {ex.Message}"); + return Results.Error(localizationService.LocalizedStrings["permDeny"] + ex.Message); } catch (Exception ex) { - return Results.Error($"An error occurred while copying the file: {ex.Message}"); + return Results.Error(localizationService.LocalizedStrings["copyError"] + ex.Message); } return Results.Ok(); @@ -174,7 +189,7 @@ public async Task TryGenerateDataIndexAsync() { var gameDirPath = configService.Config.GameDirPath; if (string.IsNullOrWhiteSpace(gameDirPath)) - return Results.Error("Game directory not set"); + return Results.Error(localizationService.LocalizedStrings["gameNotFound"]); try { @@ -183,11 +198,11 @@ public async Task TryGenerateDataIndexAsync() } catch (ArgumentNullException ex) { - return Results.Error($"Invalid data.i file: {ex.Message}"); + return Results.Error(localizationService.LocalizedStrings["invalidDataI"] + ex.Message); } catch (Exception ex) { - return Results.Error($"An error occurred while generating data.i file: {ex.Message}"); + return Results.Error(localizationService.LocalizedStrings["errGenDataI"] + ex.Message); } return Results.Ok(); diff --git a/ViewModels/ModListViewModel.cs b/ViewModels/ModListViewModel.cs index 53724ba..c76c86f 100644 --- a/ViewModels/ModListViewModel.cs +++ b/ViewModels/ModListViewModel.cs @@ -5,8 +5,11 @@ using System.Reactive.Linq; using System.Threading.Tasks; using System.Windows.Input; +using Avalonia.Controls; using DynamicData.Binding; +using MsBox.Avalonia; using ReactiveUI; +using RelinkModOrganizer.Helpers; using RelinkModOrganizer.Mappers; using RelinkModOrganizer.Services; @@ -18,18 +21,15 @@ public class ModListViewModel : ViewModelBase private readonly ModificationService _modificationService; //private readonly IFileProvider _fileProvider; - private readonly DialogService _dialogService; public ModListViewModel( ConfigurationService configService, - ModificationService modification, - //IFileProvider fileProvider, - DialogService dialogService) + ModificationService modification) + //IFileProvider fileProvider { _configService = configService; _modificationService = modification; //_fileProvider = fileProvider; - _dialogService = dialogService; LoadModItems(); @@ -82,7 +82,7 @@ private static void OpenModsFolderHandler() private async void ReloadModsCommandHandler() { (await _modificationService.LoadModsFromDiskAsync()) - .Except(_dialogService.ShowDialog); + .Except(async (err) => await MessageBoxHelpers.ErrorAsync(err)); LoadModItems(); } @@ -108,38 +108,52 @@ private async Task ModItHandlerAsync() { if (string.IsNullOrWhiteSpace(_configService.Config.GameDirPath)) { - _dialogService.ShowDialog("Please locate game in Settings first"); + await MessageBoxHelpers.ErrorAsync(Ls["gameNotFound"]); return; } // check if there are any conflict mods, then alert var conflicMods = _modificationService.GetConflicMods(); - if (conflicMods != null && conflicMods.Count != 0) + if (conflicMods is { Count: > 0 }) { var messages = conflicMods.Select(m => -$@"Found conflicts between: - -{string.Join(", \n", m)}, - -please ensure only one of them is enabled. -"); - _dialogService.ShowDialog(messages); - return; + $""" + {Ls["conflictsContent1"]} + {string.Join(", \n", m)}, + {Ls["conflictsContent2"]} + """); + var message = string.Join("\n\n", messages); + var msBoxParams = new MsBox.Avalonia.Dto.MessageBoxCustomParams + { + ButtonDefinitions = [new() { Name = Ls["ok"] }, new() { Name = Ls["cancel"] }], + ContentTitle = Ls["conflictsTitle"], + ContentMessage = message, + Icon = MsBox.Avalonia.Enums.Icon.Warning, + Topmost = true, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + var msBox = MessageBoxManager.GetMessageBoxCustom(msBoxParams); + var msBoxResult = await msBox.ShowAsync(); + if (msBoxResult == Ls["cancel"]) + return; } if (!_modificationService.TryCleanUpGameDir() - .Except(_dialogService.ShowDialog).Success) + .Except(async (err) => { await MessageBoxHelpers.ErrorAsync(err); }) + .Success) return; if (!(await _modificationService.TryCopyModsAsync()) - .Except(_dialogService.ShowDialog).Success) + .Except(async (err) => { await MessageBoxHelpers.ErrorAsync(err); }) + .Success) return; if (!(await _modificationService.TryGenerateDataIndexAsync()) - .Except(_dialogService.ShowDialog).Success) + .Except(async (err) => { await MessageBoxHelpers.ErrorAsync(err); }) + .Success) return; - _dialogService.ShowDialog("Mods installed successfully"); + await MessageBoxHelpers.InfoAsync(Ls["modInstallSuccess"]); } //private void WatchModsDirectory() diff --git a/ViewModels/SettingsViewModel.cs b/ViewModels/SettingsViewModel.cs index a0347aa..0963eb3 100644 --- a/ViewModels/SettingsViewModel.cs +++ b/ViewModels/SettingsViewModel.cs @@ -6,10 +6,10 @@ using System.Threading.Tasks; using System.Windows.Input; using Avalonia; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; using ReactiveUI; +using RelinkModOrganizer.Helpers; using RelinkModOrganizer.Services; namespace RelinkModOrganizer.ViewModels; @@ -17,22 +17,20 @@ namespace RelinkModOrganizer.ViewModels; public class SettingsViewModel : ViewModelBase { private string? _gameDirPath; - //private ComboBoxItem? _selectedLanguage; + private LanguageItemViewModel? _selectedLanguage; + private readonly ConfigurationService _configurationService; private readonly ModificationService _modificationService; - private readonly DialogService _dialogService; private readonly LocalizationService _localizationService; public SettingsViewModel( ConfigurationService configurationService, ModificationService modificationService, - DialogService dialogService, LocalizationService localizationService) { _configurationService = configurationService; _modificationService = modificationService; - _dialogService = dialogService; _localizationService = localizationService; LocateGameCommand = ReactiveCommand.CreateFromTask(LocateGameHandlerAsync); @@ -94,7 +92,7 @@ private async Task LocateGameHandlerAsync() ProductMinorPart: >= Consts.GameMinorVer, }) { - _dialogService.ShowDialog(Ls["gameVersionUnsupported"]); + await MessageBoxHelpers.ErrorAsync(Ls["gameVersionUnsupported"]); return; }