From 11bb4283efd97e0b8c2822d250549276179507b1 Mon Sep 17 00:00:00 2001 From: OpenCode Bot Date: Fri, 20 Mar 2026 09:06:15 +0800 Subject: [PATCH] feat(#8): Add project configuration load/save - Create ProjectSettings and WindowSettings models - Implement ProjectSettingsService with async support - Support saving/loading project settings to JSON - Track recent files, open tabs, and window state - Add comprehensive unit tests - Register service in DI container Closes #8 --- .../Services/ProjectSettingsServiceTests.cs | 136 ++++++++++++++++++ .../Services/ProjectSettingsService.cs | 116 +++++++++++++++ BannerlordModEditor.UI/App.axaml.cs | 3 + 3 files changed, 255 insertions(+) create mode 100644 BannerlordModEditor.Common.Tests/Services/ProjectSettingsServiceTests.cs create mode 100644 BannerlordModEditor.Common/Services/ProjectSettingsService.cs diff --git a/BannerlordModEditor.Common.Tests/Services/ProjectSettingsServiceTests.cs b/BannerlordModEditor.Common.Tests/Services/ProjectSettingsServiceTests.cs new file mode 100644 index 00000000..0f51e78e --- /dev/null +++ b/BannerlordModEditor.Common.Tests/Services/ProjectSettingsServiceTests.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; +using BannerlordModEditor.Common.Services; + +namespace BannerlordModEditor.Common.Tests.Services +{ + public class ProjectSettingsServiceTests + { + private readonly ProjectSettingsService _service; + + public ProjectSettingsServiceTests() + { + _service = new ProjectSettingsService(); + } + + [Fact] + public void GetDefault_CreatesValidDefault() + { + var result = _service.GetDefault("/test/path"); + + Assert.Equal("/test/path", result.ProjectPath); + Assert.Equal("path", result.ProjectName); + Assert.Equal("Latest", result.GameVersion); + Assert.Empty(result.RecentFiles); + Assert.Empty(result.OpenTabs); + Assert.NotNull(result.Window); + } + + [Fact] + public void GetSettingsFilePath_ReturnsCorrectPath() + { + var result = _service.GetSettingsFilePath("/test/project"); + + Assert.Equal("/test/project/.bannerlordmodeditor.json", result); + } + + [Fact] + public void Load_ReturnsDefaultForNonExistentFile() + { + var result = _service.Load("/nonexistent/path"); + + Assert.NotNull(result); + Assert.Equal("/nonexistent/path", result.ProjectPath); + } + + [Fact] + public async Task LoadAsync_ReturnsDefaultForNonExistentFile() + { + var result = await _service.LoadAsync("/nonexistent/path"); + + Assert.NotNull(result); + Assert.Equal("/nonexistent/path", result.ProjectPath); + } + + [Fact] + public void Save_CreatesSettingsFile() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var settings = new ProjectSettings + { + ProjectName = "Test Project", + GameVersion = "v1.2.9" + }; + + try + { + _service.Save(settings, tempDir); + + var settingsPath = Path.Combine(tempDir, ".bannerlordmodeditor.json"); + Assert.True(File.Exists(settingsPath)); + + var loaded = _service.Load(tempDir); + Assert.Equal("Test Project", loaded.ProjectName); + Assert.Equal("v1.2.9", loaded.GameVersion); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public async Task SaveAsync_CreatesSettingsFile() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var settings = _service.GetDefault(tempDir); + settings.ProjectName = "Async Test"; + + try + { + await _service.SaveAsync(settings, tempDir); + + var loaded = await _service.LoadAsync(tempDir); + Assert.Equal("Async Test", loaded.ProjectName); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void Save_PreservesRecentFiles() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var settings = new ProjectSettings + { + ProjectName = "Test", + RecentFiles = new System.Collections.Generic.List + { + "/path/to/file1.xml", + "/path/to/file2.xml" + } + }; + + try + { + _service.Save(settings, tempDir); + + var loaded = _service.Load(tempDir); + Assert.Equal(2, loaded.RecentFiles.Count); + Assert.Contains("/path/to/file1.xml", loaded.RecentFiles); + Assert.Contains("/path/to/file2.xml", loaded.RecentFiles); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + } +} diff --git a/BannerlordModEditor.Common/Services/ProjectSettingsService.cs b/BannerlordModEditor.Common/Services/ProjectSettingsService.cs new file mode 100644 index 00000000..cc0fe430 --- /dev/null +++ b/BannerlordModEditor.Common/Services/ProjectSettingsService.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +namespace BannerlordModEditor.Common.Services +{ + public class ProjectSettings + { + public string ProjectName { get; set; } = string.Empty; + public string ProjectPath { get; set; } = string.Empty; + public string GameVersion { get; set; } = "Latest"; + public string LastOpenedFile { get; set; } = string.Empty; + public DateTime LastOpened { get; set; } + public Dictionary CustomSettings { get; set; } = new(); + public List RecentFiles { get; set; } = new(); + public List OpenTabs { get; set; } = new(); + public WindowSettings Window { get; set; } = new(); + } + + public class WindowSettings + { + public double Width { get; set; } = 1200; + public double Height { get; set; } = 800; + public double X { get; set; } + public double Y { get; set; } + public bool IsMaximized { get; set; } + } + + public interface IProjectSettingsService + { + Task LoadAsync(string projectPath); + ProjectSettings Load(string projectPath); + Task SaveAsync(ProjectSettings settings, string projectPath); + void Save(ProjectSettings settings, string projectPath); + ProjectSettings GetDefault(string projectPath); + string GetSettingsFilePath(string projectPath); + } + + public class ProjectSettingsService : IProjectSettingsService + { + private const string SettingsFileName = ".bannerlordmodeditor.json"; + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public async Task LoadAsync(string projectPath) + { + return await Task.Run(() => Load(projectPath)); + } + + public ProjectSettings Load(string projectPath) + { + var settingsPath = GetSettingsFilePath(projectPath); + + if (!File.Exists(settingsPath)) + return GetDefault(projectPath); + + try + { + var json = File.ReadAllText(settingsPath); + var settings = JsonSerializer.Deserialize(json, JsonOptions); + return settings ?? GetDefault(projectPath); + } + catch + { + return GetDefault(projectPath); + } + } + + public async Task SaveAsync(ProjectSettings settings, string projectPath) + { + await Task.Run(() => Save(settings, projectPath)); + } + + public void Save(ProjectSettings settings, string projectPath) + { + if (settings == null) + throw new ArgumentNullException(nameof(settings)); + + var settingsPath = GetSettingsFilePath(projectPath); + var directory = Path.GetDirectoryName(settingsPath); + + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + settings.ProjectPath = projectPath; + settings.LastOpened = DateTime.Now; + + var json = JsonSerializer.Serialize(settings, JsonOptions); + File.WriteAllText(settingsPath, json); + } + + public ProjectSettings GetDefault(string projectPath) + { + return new ProjectSettings + { + ProjectPath = projectPath, + ProjectName = Path.GetFileName(projectPath), + GameVersion = "Latest", + LastOpened = DateTime.Now, + RecentFiles = new List(), + OpenTabs = new List(), + Window = new WindowSettings() + }; + } + + public string GetSettingsFilePath(string projectPath) + { + return Path.Combine(projectPath, SettingsFileName); + } + } +} diff --git a/BannerlordModEditor.UI/App.axaml.cs b/BannerlordModEditor.UI/App.axaml.cs index 028d620f..63290753 100644 --- a/BannerlordModEditor.UI/App.axaml.cs +++ b/BannerlordModEditor.UI/App.axaml.cs @@ -71,6 +71,9 @@ private IServiceCollection ConfigureServices() // 注册Common层服务 services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // 注册所有编辑器ViewModel和View services.AddTransient();