mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
* fix.hy3试图修复中 * Resolve dev paths and fix splash UI thread Compute a solutionRoot and expand development search paths (LanMountainDesktop and dev-test) in DeploymentLocator, add logging when scanning/finding hosts, and return distinct full paths. Ensure backward-compatible path checks. Fix cross-thread UI calls: invoke splashWindow.DismissAsync on the UI thread in LauncherFlowCoordinator, and make SplashWindow.DismissAsync ensure it runs on the UI thread before closing (simplified Close call). These changes improve development host discovery and prevent UI-thread access issues during shutdown. * Add configurable data location (portable/system) Introduce support for choosing and resolving the application's data root (system user dir vs. portable app folder). Adds DataLocationConfig model, DataLocationResolver (load/save/resolve/migrate), a UI prompt (DataLocationPromptWindow) and an OOBE step (DataLocationOobeStep) to let users pick and optionally migrate existing data. Wire the chosen data root into the launcher flow and host launch plan (forwarded via --data-root and LMD_DATA_ROOT), and add AppDataPathProvider to let runtime services read the effective data root (initialized in Program.Main). Update various services (logging, settings, DB, plugin/market, startup registry, etc.) to use the new provider/resolver and register the config type in the JSON context. This enables portable installs, safe migration, and runtime overrides via CLI or environment variable. * Add dev/debug startup flow and launch profiles Handle design-time initialization and add a developer debug startup path: App now skips normal startup when in design mode and shows a DevDebugWindow when running in debug (unless a preview or apply-update command). CommandContext.IsDebugMode is extended to include DOTNET_ENVIRONMENT=Development via a new IsDevelopmentEnvironment helper. Program.Main and BuildAvaloniaApp are made public to aid tooling. Added multiple launchSettings profiles for debug and preview commands that set DOTNET_ENVIRONMENT=Development to simplify IDE debugging and UI previewing. * Simplify splash to fade; add themed about banners Simplify splash startup visuals by removing the multi-mode/slide behavior and always using a fade animation. Update App to create SplashWindow without a StartupVisualMode parameter and remove related fields, layout configuration, slide animation, and easing helpers from SplashWindow. Clean up unused using. Replace the single about_banner asset with theme-aware variants (about_banner_dark.png and about_banner_light.png), delete the old about_banner.png, and update AboutSettingsPage to use a DynamicResource ImageBrush (AboutBannerBrush) that selects the appropriate banner per theme. * Use AppJsonContext for startup state serialization Switch serialization to the source-generated System.Text.Json context: add JsonSerializable(typeof(StartupAttemptRecord)) to AppJsonContext and replace the previous JsonSerializerOptions-based Serialize/Deserialize calls with AppJsonContext.Default.StartupAttemptRecord. Also remove the now-unused SerializerOptions field. Additionally, update .gitignore to exclude /test-aot-publish. * Add OOBE redesign, theme & data location support Introduce a redesigned OOBE flow and data-location/theme support across the launcher. Adds a new ThemeService for applying light/dark and accent colors; integrates FluentIcons.Avalonia package for icons. Overhauls OobeWindow (UX animations, typing effect, multi-step theme and data-location pages, Monet options, and final welcome step) and its code-behind to handle step navigation, accent selection, and data-location resolution. Adds DataLocation UI and handlers (DataLocationPromptWindow changes, DataLocation resolver usage) and wires a DevDebug UI for toggling/opening the data-location page. UpdateEngineService now resolves the launcher root via DataLocationResolver. Misc: update various view models, localization entries and remove TrimmerRoots.xml. * Refactor data location paths and add background service Refactor DataLocationResolver to centralize data path resolution (ResolveLauncherDataPath, ResolveDesktopDataPath, ResolveConfigPath, ResolveLauncherLogsPath, ResolveLauncherStatePath) and replace usages of the previous ".launcher" layout with a "Launcher" folder. Update API: LoadConfig/SaveConfig reorganized and ApplyLocationChoice now accepts an optional custom path and migration flag; migration logic updated accordingly. Update dependent services and views (Logger, DeploymentLocator, UpdateEngineService, OobeStateService, StartupAttemptRegistry, LauncherDebugSettingsStore, OobeWindow) to use the new resolver APIs and paths. Add LauncherBackgroundService to load/validate/cache a custom splash background image and wire it into SplashWindow (AXAML/Axaml.cs) with UI placeholders and overlay. Misc: minor cleanup of Oobe/Splash XAML and related code adjustments and logging improvements.
271 lines
7.8 KiB
C#
271 lines
7.8 KiB
C#
using System.Text.Json;
|
||
using LanMountainDesktop.Launcher.Models;
|
||
|
||
namespace LanMountainDesktop.Launcher.Services;
|
||
|
||
internal sealed class DataLocationResolver
|
||
{
|
||
private const string ConfigFileName = "data-location.config.json";
|
||
private const string LauncherFolderName = "Launcher";
|
||
private const string DesktopFolderName = "Desktop";
|
||
|
||
private readonly string _appRoot;
|
||
private readonly string _defaultSystemDataPath;
|
||
|
||
public DataLocationResolver(string appRoot)
|
||
{
|
||
_appRoot = Path.GetFullPath(appRoot);
|
||
_defaultSystemDataPath = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"LanMountainDesktop");
|
||
}
|
||
|
||
public string AppRoot => _appRoot;
|
||
|
||
/// <summary>
|
||
/// 默认系统数据路径(用户目录)
|
||
/// </summary>
|
||
public string DefaultSystemDataPath => _defaultSystemDataPath;
|
||
|
||
/// <summary>
|
||
/// 默认便携模式数据路径(应用目录下的 AppData)
|
||
/// </summary>
|
||
public string DefaultPortableDataPath => Path.Combine(_appRoot, "AppData");
|
||
|
||
/// <summary>
|
||
/// 检查是否允许便携模式(应用目录是否可写)
|
||
/// </summary>
|
||
public bool IsPortableModeAllowed()
|
||
{
|
||
try
|
||
{
|
||
var testFile = Path.Combine(_appRoot, $".write-test-{Guid.NewGuid():N}.tmp");
|
||
File.WriteAllText(testFile, string.Empty);
|
||
File.Delete(testFile);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析数据根目录(用户选择的位置)
|
||
/// </summary>
|
||
public string ResolveDataRoot()
|
||
{
|
||
var config = LoadConfig();
|
||
if (config is null)
|
||
{
|
||
return _defaultSystemDataPath;
|
||
}
|
||
|
||
if (string.Equals(config.DataLocationMode, "Portable", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var portablePath = !string.IsNullOrWhiteSpace(config.PortableDataPath)
|
||
? config.PortableDataPath
|
||
: _defaultSystemDataPath;
|
||
return Path.GetFullPath(portablePath);
|
||
}
|
||
|
||
return !string.IsNullOrWhiteSpace(config.SystemDataPath)
|
||
? Path.GetFullPath(config.SystemDataPath)
|
||
: _defaultSystemDataPath;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动器数据目录(日志、配置、状态等)
|
||
/// </summary>
|
||
public string ResolveLauncherDataPath()
|
||
{
|
||
return Path.Combine(ResolveDataRoot(), LauncherFolderName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 桌面应用数据目录(组件、设置、插件等)
|
||
/// </summary>
|
||
public string ResolveDesktopDataPath()
|
||
{
|
||
return Path.Combine(ResolveDataRoot(), DesktopFolderName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 数据位置配置文件路径(保存在 Launcher 目录下)
|
||
/// </summary>
|
||
public string ResolveConfigPath()
|
||
{
|
||
return Path.Combine(ResolveLauncherDataPath(), ConfigFileName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动器日志目录
|
||
/// </summary>
|
||
public string ResolveLauncherLogsPath()
|
||
{
|
||
return Path.Combine(ResolveLauncherDataPath(), "logs");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动器状态目录
|
||
/// </summary>
|
||
public string ResolveLauncherStatePath()
|
||
{
|
||
return Path.Combine(ResolveLauncherDataPath(), "state");
|
||
}
|
||
|
||
public DataLocationMode ResolveMode()
|
||
{
|
||
var config = LoadConfig();
|
||
if (config is null)
|
||
{
|
||
return DataLocationMode.System;
|
||
}
|
||
|
||
return string.Equals(config.DataLocationMode, "Portable", StringComparison.OrdinalIgnoreCase)
|
||
? DataLocationMode.Portable
|
||
: DataLocationMode.System;
|
||
}
|
||
|
||
public DataLocationConfig? LoadConfig()
|
||
{
|
||
try
|
||
{
|
||
var configPath = ResolveConfigPath();
|
||
if (!File.Exists(configPath))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var json = File.ReadAllText(configPath);
|
||
return JsonSerializer.Deserialize(json, AppJsonContext.Default.DataLocationConfig);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Warn($"Failed to load data location config. Error='{ex.Message}'.");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public bool SaveConfig(DataLocationConfig config)
|
||
{
|
||
try
|
||
{
|
||
var launcherPath = ResolveLauncherDataPath();
|
||
Directory.CreateDirectory(launcherPath);
|
||
|
||
var configPath = ResolveConfigPath();
|
||
var json = JsonSerializer.Serialize(config, AppJsonContext.Default.DataLocationConfig);
|
||
File.WriteAllText(configPath, json);
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Warn($"Failed to save data location config. Error='{ex.Message}'.");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public bool ApplyLocationChoice(DataLocationMode mode, string? customPath = null, bool migrateExistingData = false)
|
||
{
|
||
var targetDataRoot = mode == DataLocationMode.Portable && !string.IsNullOrWhiteSpace(customPath)
|
||
? Path.GetFullPath(customPath)
|
||
: _defaultSystemDataPath;
|
||
|
||
var config = new DataLocationConfig
|
||
{
|
||
DataLocationMode = mode.ToString(),
|
||
SystemDataPath = _defaultSystemDataPath,
|
||
PortableDataPath = mode == DataLocationMode.Portable ? targetDataRoot : null
|
||
};
|
||
|
||
// 先创建目录结构
|
||
try
|
||
{
|
||
Directory.CreateDirectory(ResolveLauncherDataPath());
|
||
Directory.CreateDirectory(ResolveDesktopDataPath());
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Warn($"Failed to create data directories. Error='{ex.Message}'.");
|
||
return false;
|
||
}
|
||
|
||
// 保存配置
|
||
if (!SaveConfig(config))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (migrateExistingData && mode == DataLocationMode.Portable)
|
||
{
|
||
MigrateSystemDataToPortable(targetDataRoot);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool HasExistingSystemData()
|
||
{
|
||
var desktopPath = Path.Combine(_defaultSystemDataPath, DesktopFolderName);
|
||
if (!Directory.Exists(desktopPath))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var markerFiles = new[]
|
||
{
|
||
Path.Combine(desktopPath, "settings.json"),
|
||
Path.Combine(desktopPath, "component-state.db"),
|
||
Path.Combine(desktopPath, "app.db")
|
||
};
|
||
|
||
return markerFiles.Any(File.Exists);
|
||
}
|
||
|
||
private void MigrateSystemDataToPortable(string targetDataRoot)
|
||
{
|
||
if (!HasExistingSystemData())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var sourceDesktopPath = Path.Combine(_defaultSystemDataPath, DesktopFolderName);
|
||
var targetDesktopPath = Path.Combine(targetDataRoot, DesktopFolderName);
|
||
|
||
try
|
||
{
|
||
Directory.CreateDirectory(targetDesktopPath);
|
||
|
||
// 迁移桌面数据
|
||
if (Directory.Exists(sourceDesktopPath))
|
||
{
|
||
CopyDirectory(sourceDesktopPath, targetDesktopPath);
|
||
}
|
||
|
||
Logger.Info($"Data migration completed. Target='{targetDataRoot}'.");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Warn($"Data migration failed. Target='{targetDataRoot}'. Error='{ex.Message}'.");
|
||
}
|
||
}
|
||
|
||
private static void CopyDirectory(string sourceDir, string destDir)
|
||
{
|
||
Directory.CreateDirectory(destDir);
|
||
|
||
foreach (var file in Directory.GetFiles(sourceDir))
|
||
{
|
||
var destFile = Path.Combine(destDir, Path.GetFileName(file));
|
||
File.Copy(file, destFile, overwrite: true);
|
||
}
|
||
|
||
foreach (var subDir in Directory.GetDirectories(sourceDir))
|
||
{
|
||
var destSubDir = Path.Combine(destDir, Path.GetFileName(subDir));
|
||
CopyDirectory(subDir, destSubDir);
|
||
}
|
||
}
|
||
}
|