setting_re3

This commit is contained in:
lincube
2026-03-13 09:10:00 +08:00
parent c4df243610
commit 3b3f060f33
70 changed files with 1986 additions and 8966 deletions

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public interface IPluginSettingsService
{
string PluginId { get; }
IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId);
T LoadComponentSection<T>(string componentId, string? placementId, string sectionId) where T : new();
void SaveComponentSection<T>(
string componentId,
string? placementId,
string sectionId,
T section,
IReadOnlyCollection<string>? changedKeys = null);
void DeleteComponentSection(string componentId, string? placementId, string sectionId);
}

View File

@@ -10,7 +10,8 @@ public sealed class PluginDesktopComponentContext
IReadOnlyDictionary<string, object?> properties, IReadOnlyDictionary<string, object?> properties,
string componentId, string componentId,
string? placementId, string? placementId,
double cellSize) double cellSize,
IPluginSettingsService? pluginSettings = null)
{ {
ArgumentNullException.ThrowIfNull(manifest); ArgumentNullException.ThrowIfNull(manifest);
ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory); ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory);
@@ -27,6 +28,7 @@ public sealed class PluginDesktopComponentContext
ComponentId = componentId.Trim(); ComponentId = componentId.Trim();
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim(); PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
CellSize = Math.Max(1, cellSize); CellSize = Math.Max(1, cellSize);
PluginSettings = pluginSettings;
} }
public PluginManifest Manifest { get; } public PluginManifest Manifest { get; }
@@ -45,6 +47,8 @@ public sealed class PluginDesktopComponentContext
public double CellSize { get; } public double CellSize { get; }
public IPluginSettingsService? PluginSettings { get; }
public T? GetService<T>() public T? GetService<T>()
{ {
return (T?)Services.GetService(typeof(T)); return (T?)Services.GetService(typeof(T));

View File

@@ -10,8 +10,10 @@ using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using AvaloniaWebView; using AvaloniaWebView;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels; using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views; using LanMountainDesktop.Views;
@@ -33,7 +35,7 @@ public partial class App : Application
RestartRequested = 2 RestartRequested = 2
} }
private readonly AppSettingsService _appSettingsService = new(); private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService(); private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
private bool _exitCleanupCompleted; private bool _exitCleanupCompleted;
@@ -50,6 +52,7 @@ public partial class App : Application
(Current as App)?._hostApplicationLifecycle; (Current as App)?._hostApplicationLifecycle;
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService; public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
public ISettingsFacadeService SettingsFacade => _settingsFacade;
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle; public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
internal void OpenIndependentSettingsModule(string source, string? pageTag = null) internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
@@ -159,7 +162,8 @@ public partial class App : Application
try try
{ {
_pluginRuntimeService?.Dispose(); _pluginRuntimeService?.Dispose();
_pluginRuntimeService = new PluginRuntimeService(); _pluginRuntimeService = new PluginRuntimeService(_settingsFacade);
HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService);
_pluginRuntimeService.LoadInstalledPlugins(); _pluginRuntimeService.LoadInstalledPlugins();
} }
catch (Exception ex) catch (Exception ex)
@@ -564,7 +568,7 @@ public partial class App : Application
private string L(string key, string fallback) private string L(string key, string fallback)
{ {
var snapshot = _appSettingsService.Load(); var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode); var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
return _localizationService.GetString(languageCode, key, fallback); return _localizationService.GetString(languageCode, key, fallback);
} }

View File

@@ -1,8 +1,9 @@
using LanMountainDesktop.Services; using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.ComponentSystem; namespace LanMountainDesktop.ComponentSystem;
public sealed record DesktopComponentRuntimeContext( public sealed record DesktopComponentRuntimeContext(
string ComponentId, string ComponentId,
string? PlacementId, string? PlacementId,
IComponentInstanceSettingsStore ComponentSettingsStore); ISettingsService SettingsService,
IComponentSettingsAccessor ComponentSettingsAccessor);

View File

@@ -1,8 +0,0 @@
using LanMountainDesktop.Services;
namespace LanMountainDesktop.ComponentSystem;
public interface IComponentSettingsStoreAware
{
void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore);
}

View File

@@ -4,7 +4,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.WebView.Desktop; using Avalonia.WebView.Desktop;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop; namespace LanMountainDesktop;
@@ -121,7 +123,10 @@ sealed class Program
{ {
try try
{ {
return AppRenderingModeHelper.Normalize(new AppSettingsService().Load().AppRenderMode); var snapshot = HostSettingsFacadeProvider.GetOrCreate()
.Settings
.LoadSnapshot<AppSettingsSnapshot>(LanMountainDesktop.PluginSdk.SettingsScope.App);
return AppRenderingModeHelper.Normalize(snapshot.AppRenderMode);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Views;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Services;
public sealed record ComponentLibraryCreateContext(
double CellSize,
TimeZoneService TimeZoneService,
IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService,
ICalculatorDataService CalculatorDataService,
string? PlacementId = null);
public interface IComponentLibraryService
{
IReadOnlyList<DesktopComponentDefinition> GetDefinitions();
bool TryCreateControl(
string componentId,
ComponentLibraryCreateContext context,
out Control? control,
out Exception? exception);
}
public interface IComponentLibraryWindowService
{
void Open(MainWindow window);
void Close(MainWindow window);
void Toggle(MainWindow window);
}
internal sealed class ComponentLibraryService : IComponentLibraryService
{
private readonly ComponentRegistry _registry;
private readonly DesktopComponentRuntimeRegistry _runtimeRegistry;
public ComponentLibraryService(ComponentRegistry registry, DesktopComponentRuntimeRegistry runtimeRegistry)
{
_registry = registry;
_runtimeRegistry = runtimeRegistry;
}
public IReadOnlyList<DesktopComponentDefinition> GetDefinitions()
{
return _registry.GetAll().ToArray();
}
public bool TryCreateControl(
string componentId,
ComponentLibraryCreateContext context,
out Control? control,
out Exception? exception)
{
control = null;
exception = null;
if (!_runtimeRegistry.TryGetDescriptor(componentId, out var descriptor))
{
return false;
}
try
{
control = descriptor.CreateControl(
context.CellSize,
context.TimeZoneService,
context.WeatherInfoService,
context.RecommendationInfoService,
context.CalculatorDataService,
context.PlacementId);
return true;
}
catch (Exception ex)
{
exception = ex;
return false;
}
}
}
internal sealed class ComponentLibraryWindowService : IComponentLibraryWindowService
{
public void Open(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
window.OpenComponentLibraryWindowFromService();
}
public void Close(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
window.CloseComponentLibraryWindowFromService();
}
public void Toggle(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
if (window.IsComponentLibraryOpenFromService)
{
window.CloseComponentLibraryWindowFromService();
return;
}
window.OpenComponentLibraryWindowFromService();
}
}

View File

@@ -1,40 +1,22 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.Json;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services; namespace LanMountainDesktop.Services;
public sealed class ComponentSettingsService : IComponentInstanceSettingsStore public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
{ {
private static readonly JsonSerializerOptions SerializerOptions = new() private const string LegacySectionId = "__legacy__";
{ private readonly ISettingsService? _settingsService;
PropertyNameCaseInsensitive = true, private readonly IComponentStateStore? _stateStore;
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, private readonly IComponentMessageStore? _messageStore;
WriteIndented = true
};
private static readonly object CacheGate = new();
private static readonly TimeSpan CacheProbeInterval = TimeSpan.FromMilliseconds(400);
private static string? _cachedPath;
private static ComponentSettingsDocumentSnapshot? _cachedSnapshot;
private static DateTime _cachedWriteTimeUtc = DateTime.MinValue;
private static DateTime _lastProbeUtc = DateTime.MinValue;
private readonly string _settingsPath;
private readonly string _legacyAppSettingsPath;
private string _scopedComponentId = string.Empty; private string _scopedComponentId = string.Empty;
private string _scopedPlacementId = string.Empty; private string _scopedPlacementId = string.Empty;
public ComponentSettingsService() public ComponentSettingsService()
: this(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop"))
{ {
_settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
} }
internal ComponentSettingsService(string settingsDirectory) internal ComponentSettingsService(string settingsDirectory)
@@ -44,8 +26,9 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
throw new ArgumentException("Settings directory cannot be null or whitespace.", nameof(settingsDirectory)); throw new ArgumentException("Settings directory cannot be null or whitespace.", nameof(settingsDirectory));
} }
_settingsPath = Path.Combine(settingsDirectory, "component-settings.json"); var storage = new SqliteComponentDomainStorage(settingsDirectory);
_legacyAppSettingsPath = Path.Combine(settingsDirectory, "settings.json"); _stateStore = storage;
_messageStore = storage;
} }
public ComponentSettingsSnapshot Load() public ComponentSettingsSnapshot Load()
@@ -55,19 +38,15 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
return LoadForComponent(_scopedComponentId, _scopedPlacementId); return LoadForComponent(_scopedComponentId, _scopedPlacementId);
} }
try if (_settingsService is not null)
{ {
lock (CacheGate) return _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
{ SettingsScope.ComponentInstance,
var document = LoadDocumentLocked(); subjectId: string.Empty,
return document.DefaultSettings.Clone(); placementId: null);
}
}
catch (Exception ex)
{
AppLogger.Warn("ComponentSettings", $"Failed to load component settings from '{_settingsPath}'.", ex);
return new ComponentSettingsSnapshot();
} }
return _stateStore?.LoadState(componentId: string.Empty, placementId: null) ?? new ComponentSettingsSnapshot();
} }
public void Save(ComponentSettingsSnapshot snapshot) public void Save(ComponentSettingsSnapshot snapshot)
@@ -78,186 +57,116 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
return; return;
} }
var snapshotToPersist = NormalizeSnapshot(snapshot); if (_settingsService is not null)
{
_settingsService.SaveSnapshot(
SettingsScope.ComponentInstance,
snapshot ?? new ComponentSettingsSnapshot(),
subjectId: string.Empty,
placementId: null);
return;
}
try _stateStore?.SaveState(componentId: string.Empty, placementId: null, snapshot ?? new ComponentSettingsSnapshot());
{
lock (CacheGate)
{
var document = LoadDocumentLocked();
document.DefaultSettings = snapshotToPersist;
PersistDocumentLocked(document);
}
}
catch (Exception ex)
{
AppLogger.Warn("ComponentSettings", $"Failed to save default component settings to '{_settingsPath}'.", ex);
}
} }
public ComponentSettingsSnapshot LoadForComponent(string componentId, string? placementId) public ComponentSettingsSnapshot LoadForComponent(string componentId, string? placementId)
{ {
try if (_settingsService is not null)
{ {
lock (CacheGate) return _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
{ SettingsScope.ComponentInstance,
var document = LoadDocumentLocked(); subjectId: componentId,
var instanceKey = BuildInstanceKey(componentId, placementId); placementId: placementId);
if (!string.IsNullOrWhiteSpace(instanceKey) && }
document.InstanceSettings.TryGetValue(instanceKey, out var snapshot))
{
return snapshot.Clone();
}
return document.DefaultSettings.Clone(); return _stateStore?.LoadState(componentId, placementId) ?? new ComponentSettingsSnapshot();
}
}
catch (Exception ex)
{
AppLogger.Warn(
"ComponentSettings",
$"Failed to load component settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}",
ex);
return new ComponentSettingsSnapshot();
}
} }
public void SaveForComponent(string componentId, string? placementId, ComponentSettingsSnapshot snapshot) public void SaveForComponent(string componentId, string? placementId, ComponentSettingsSnapshot snapshot)
{ {
var normalizedSnapshot = NormalizeSnapshot(snapshot); if (_settingsService is not null)
var instanceKey = BuildInstanceKey(componentId, placementId);
if (string.IsNullOrWhiteSpace(instanceKey))
{ {
Save(normalizedSnapshot); _settingsService.SaveSnapshot(
SettingsScope.ComponentInstance,
snapshot ?? new ComponentSettingsSnapshot(),
subjectId: componentId,
placementId: placementId);
return; return;
} }
try _stateStore?.SaveState(componentId, placementId, snapshot ?? new ComponentSettingsSnapshot());
{
lock (CacheGate)
{
var document = LoadDocumentLocked();
document.InstanceSettings[instanceKey] = normalizedSnapshot;
PersistDocumentLocked(document);
}
}
catch (Exception ex)
{
AppLogger.Warn(
"ComponentSettings",
$"Failed to save component settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}",
ex);
}
} }
public void DeleteForComponent(string componentId, string? placementId) public void DeleteForComponent(string componentId, string? placementId)
{ {
var instanceKey = BuildInstanceKey(componentId, placementId); if (_settingsService is not null)
if (string.IsNullOrWhiteSpace(instanceKey))
{ {
_settingsService.SaveSnapshot(
SettingsScope.ComponentInstance,
new ComponentSettingsSnapshot(),
subjectId: componentId,
placementId: placementId);
_settingsService.DeleteSection(SettingsScope.ComponentInstance, componentId, LegacySectionId, placementId);
return; return;
} }
try _stateStore?.DeleteState(componentId, placementId);
{
lock (CacheGate)
{
var document = LoadDocumentLocked();
var changed = document.InstanceSettings.Remove(instanceKey);
changed |= document.PluginSettings.Remove(instanceKey);
if (changed)
{
PersistDocumentLocked(document);
}
}
}
catch (Exception ex)
{
AppLogger.Warn(
"ComponentSettings",
$"Failed to delete component settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}",
ex);
}
} }
public T LoadPluginSettings<T>(string componentId, string? placementId) where T : new() public T LoadPluginSettings<T>(string componentId, string? placementId) where T : new()
{ {
try if (_settingsService is not null)
{ {
lock (CacheGate) return _settingsService.LoadSection<T>(
{ SettingsScope.ComponentInstance,
var document = LoadDocumentLocked(); subjectId: componentId,
var instanceKey = BuildInstanceKey(componentId, placementId); sectionId: LegacySectionId,
if (string.IsNullOrWhiteSpace(instanceKey) || placementId: placementId);
!document.PluginSettings.TryGetValue(instanceKey, out var settingsElement)) }
{
return new T();
}
return JsonSerializer.Deserialize<T>(settingsElement.GetRawText(), SerializerOptions) ?? new T(); if (_messageStore is SqliteComponentDomainStorage sqliteStorage)
}
}
catch (Exception ex)
{ {
AppLogger.Warn( return sqliteStorage.LoadLegacyMessage<T>(componentId, placementId);
"ComponentSettings",
$"Failed to load plugin settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}",
ex);
return new T();
} }
return new T();
} }
public void SavePluginSettings<T>(string componentId, string? placementId, T settings) public void SavePluginSettings<T>(string componentId, string? placementId, T settings)
{ {
var instanceKey = BuildInstanceKey(componentId, placementId); if (_settingsService is not null)
if (string.IsNullOrWhiteSpace(instanceKey))
{ {
_settingsService.SaveSection(
SettingsScope.ComponentInstance,
subjectId: componentId,
sectionId: LegacySectionId,
section: settings,
placementId: placementId);
return; return;
} }
try if (_messageStore is SqliteComponentDomainStorage sqliteStorage)
{ {
lock (CacheGate) sqliteStorage.SaveLegacyMessage(componentId, placementId, settings);
{
var document = LoadDocumentLocked();
document.PluginSettings[instanceKey] = JsonSerializer.SerializeToElement(settings, SerializerOptions).Clone();
PersistDocumentLocked(document);
}
}
catch (Exception ex)
{
AppLogger.Warn(
"ComponentSettings",
$"Failed to save plugin settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}",
ex);
} }
} }
public void DeletePluginSettings(string componentId, string? placementId) public void DeletePluginSettings(string componentId, string? placementId)
{ {
var instanceKey = BuildInstanceKey(componentId, placementId); if (_settingsService is not null)
if (string.IsNullOrWhiteSpace(instanceKey))
{ {
_settingsService.DeleteSection(
SettingsScope.ComponentInstance,
subjectId: componentId,
sectionId: LegacySectionId,
placementId: placementId);
return; return;
} }
try if (_messageStore is SqliteComponentDomainStorage sqliteStorage)
{ {
lock (CacheGate) sqliteStorage.DeleteLegacyMessage(componentId, placementId);
{
var document = LoadDocumentLocked();
if (document.PluginSettings.Remove(instanceKey))
{
PersistDocumentLocked(document);
}
}
}
catch (Exception ex)
{
AppLogger.Warn(
"ComponentSettings",
$"Failed to delete plugin settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}",
ex);
} }
} }
@@ -309,385 +218,9 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
} }
} }
private bool TryGetCachedWithoutProbe(DateTime nowUtc, out ComponentSettingsDocumentSnapshot snapshot) internal static void ResetCacheForTests()
{ {
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) && // no-op: SQLite storage is directly persisted without in-memory cache.
_cachedSnapshot is not null &&
nowUtc - _lastProbeUtc < CacheProbeInterval)
{
snapshot = _cachedSnapshot.Clone();
return true;
}
snapshot = null!;
return false;
}
private bool TryGetCachedAfterProbe(DateTime writeTimeUtc, out ComponentSettingsDocumentSnapshot snapshot)
{
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) &&
_cachedSnapshot is not null &&
writeTimeUtc == _cachedWriteTimeUtc)
{
snapshot = _cachedSnapshot.Clone();
return true;
}
snapshot = null!;
return false;
}
private ComponentSettingsDocumentSnapshot LoadDocumentLocked()
{
var nowUtc = DateTime.UtcNow;
if (TryGetCachedWithoutProbe(nowUtc, out var cached))
{
return cached;
}
var hasFile = File.Exists(_settingsPath);
var writeTimeUtc = hasFile
? File.GetLastWriteTimeUtc(_settingsPath)
: DateTime.MinValue;
_lastProbeUtc = nowUtc;
if (TryGetCachedAfterProbe(writeTimeUtc, out cached))
{
return cached;
}
ComponentSettingsDocumentSnapshot loadedSnapshot;
var loadDetails = ComponentSettingsLoadDetails.Empty;
if (hasFile)
{
loadDetails = LoadSnapshotFromDisk();
loadedSnapshot = loadDetails.Snapshot;
}
else if (TryLoadLegacySnapshot(out var migratedSnapshot))
{
loadedSnapshot = new ComponentSettingsDocumentSnapshot
{
DefaultSettings = NormalizeSnapshot(migratedSnapshot)
};
loadDetails = new ComponentSettingsLoadDetails(
loadedSnapshot,
ComponentSettingsDocumentFormat.LegacySnapshot,
true);
}
else
{
loadedSnapshot = new ComponentSettingsDocumentSnapshot();
}
var normalizedSnapshot = NormalizeDocument(loadedSnapshot);
if (loadDetails.ShouldRewriteToCanonical)
{
writeTimeUtc = PersistSnapshotToDisk(normalizedSnapshot);
}
LogLoadDetails(loadDetails.Format, loadDetails.ShouldRewriteToCanonical, normalizedSnapshot);
UpdateCache(normalizedSnapshot, writeTimeUtc, nowUtc);
return normalizedSnapshot.Clone();
}
private ComponentSettingsLoadDetails LoadSnapshotFromDisk()
{
try
{
var json = File.ReadAllText(_settingsPath);
using var document = JsonDocument.Parse(json);
if (TryGetDocumentFormat(document.RootElement, out var format))
{
var snapshot = JsonSerializer.Deserialize<ComponentSettingsDocumentSnapshot>(json, SerializerOptions);
return new ComponentSettingsLoadDetails(
snapshot ?? new ComponentSettingsDocumentSnapshot(),
format,
format == ComponentSettingsDocumentFormat.PascalCaseDocument);
}
var legacySnapshot = JsonSerializer.Deserialize<ComponentSettingsSnapshot>(json, SerializerOptions);
return new ComponentSettingsLoadDetails(
new ComponentSettingsDocumentSnapshot
{
DefaultSettings = NormalizeSnapshot(legacySnapshot)
},
ComponentSettingsDocumentFormat.LegacySnapshot,
true);
}
catch (Exception ex)
{
AppLogger.Warn("ComponentSettings", $"Failed to deserialize component settings from '{_settingsPath}'.", ex);
return ComponentSettingsLoadDetails.Empty;
}
}
private bool TryLoadLegacySnapshot(out ComponentSettingsSnapshot snapshot)
{
snapshot = new ComponentSettingsSnapshot();
try
{
if (!File.Exists(_legacyAppSettingsPath))
{
return false;
}
var legacyJson = File.ReadAllText(_legacyAppSettingsPath);
var legacy = JsonSerializer.Deserialize<LegacyComponentSettingsSnapshot>(legacyJson, SerializerOptions);
if (legacy is null)
{
return false;
}
snapshot = new ComponentSettingsSnapshot
{
DailyArtworkMirrorSource = legacy.DailyArtworkMirrorSource,
ImportedClassSchedules = legacy.ImportedClassSchedules ?? [],
ActiveImportedClassScheduleId = legacy.ActiveImportedClassScheduleId ?? string.Empty,
StudyEnvironmentShowDisplayDb = legacy.StudyEnvironmentShowDisplayDb,
StudyEnvironmentShowDbfs = legacy.StudyEnvironmentShowDbfs,
DesktopClockTimeZoneId = legacy.DesktopClockTimeZoneId,
DesktopClockSecondHandMode = legacy.DesktopClockSecondHandMode,
WorldClockTimeZoneIds = legacy.WorldClockTimeZoneIds ?? [],
WorldClockSecondHandMode = legacy.WorldClockSecondHandMode,
CnrDailyNewsAutoRotateEnabled = legacy.CnrDailyNewsAutoRotateEnabled,
CnrDailyNewsAutoRotateIntervalMinutes = legacy.CnrDailyNewsAutoRotateIntervalMinutes,
IfengNewsAutoRefreshEnabled = legacy.IfengNewsAutoRefreshEnabled,
IfengNewsAutoRefreshIntervalMinutes = legacy.IfengNewsAutoRefreshIntervalMinutes,
IfengNewsChannelType = legacy.IfengNewsChannelType,
DailyWordAutoRefreshEnabled = legacy.DailyWordAutoRefreshEnabled,
DailyWordAutoRefreshIntervalMinutes = legacy.DailyWordAutoRefreshIntervalMinutes,
BilibiliHotSearchAutoRefreshEnabled = legacy.BilibiliHotSearchAutoRefreshEnabled,
BilibiliHotSearchAutoRefreshIntervalMinutes = legacy.BilibiliHotSearchAutoRefreshIntervalMinutes,
BaiduHotSearchAutoRefreshEnabled = legacy.BaiduHotSearchAutoRefreshEnabled,
BaiduHotSearchAutoRefreshIntervalMinutes = legacy.BaiduHotSearchAutoRefreshIntervalMinutes,
BaiduHotSearchSourceType = legacy.BaiduHotSearchSourceType,
WeatherAutoRefreshEnabled = legacy.WeatherAutoRefreshEnabled,
WeatherAutoRefreshIntervalMinutes = legacy.WeatherAutoRefreshIntervalMinutes,
Stcn24ForumAutoRefreshEnabled = legacy.Stcn24ForumAutoRefreshEnabled,
Stcn24ForumAutoRefreshIntervalMinutes = legacy.Stcn24ForumAutoRefreshIntervalMinutes,
Stcn24ForumSourceType = legacy.Stcn24ForumSourceType
};
return true;
}
catch (Exception ex)
{
AppLogger.Warn("ComponentSettings", $"Failed to migrate legacy component settings from '{_legacyAppSettingsPath}'.", ex);
return false;
}
}
private void PersistDocumentLocked(ComponentSettingsDocumentSnapshot snapshot)
{
var writeTimeUtc = PersistSnapshotToDisk(snapshot);
UpdateCache(snapshot, writeTimeUtc, DateTime.UtcNow);
}
private DateTime PersistSnapshotToDisk(ComponentSettingsDocumentSnapshot snapshot)
{
var directory = Path.GetDirectoryName(_settingsPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(snapshot, SerializerOptions);
File.WriteAllText(_settingsPath, json);
return File.Exists(_settingsPath)
? File.GetLastWriteTimeUtc(_settingsPath)
: DateTime.UtcNow;
}
private static ComponentSettingsSnapshot NormalizeSnapshot(ComponentSettingsSnapshot? snapshot)
{
var normalized = snapshot?.Clone() ?? new ComponentSettingsSnapshot();
normalized.DailyArtworkMirrorSource = DailyArtworkMirrorSources.Normalize(normalized.DailyArtworkMirrorSource);
normalized.ImportedClassSchedules = NormalizeImportedSchedules(normalized.ImportedClassSchedules);
normalized.ActiveImportedClassScheduleId = NormalizeActiveScheduleId(
normalized.ActiveImportedClassScheduleId,
normalized.ImportedClassSchedules);
if (!normalized.StudyEnvironmentShowDisplayDb && !normalized.StudyEnvironmentShowDbfs)
{
normalized.StudyEnvironmentShowDisplayDb = true;
}
normalized.DesktopClockTimeZoneId = NormalizeDesktopClockTimeZoneId(normalized.DesktopClockTimeZoneId);
normalized.DesktopClockSecondHandMode = ClockSecondHandMode.Normalize(normalized.DesktopClockSecondHandMode);
normalized.WorldClockTimeZoneIds = WorldClockTimeZoneCatalog
.NormalizeTimeZoneIds(normalized.WorldClockTimeZoneIds)
.ToList();
normalized.WorldClockSecondHandMode = ClockSecondHandMode.Normalize(normalized.WorldClockSecondHandMode);
normalized.CnrDailyNewsAutoRotateIntervalMinutes = NormalizeCnrInterval(normalized.CnrDailyNewsAutoRotateIntervalMinutes);
normalized.IfengNewsAutoRefreshIntervalMinutes = NormalizeIfengNewsInterval(normalized.IfengNewsAutoRefreshIntervalMinutes);
normalized.IfengNewsChannelType = IfengNewsChannelTypes.Normalize(normalized.IfengNewsChannelType);
normalized.DailyWordAutoRefreshIntervalMinutes = NormalizeDailyWordInterval(normalized.DailyWordAutoRefreshIntervalMinutes);
normalized.BilibiliHotSearchAutoRefreshIntervalMinutes = NormalizeBilibiliHotSearchInterval(
normalized.BilibiliHotSearchAutoRefreshIntervalMinutes);
normalized.BaiduHotSearchAutoRefreshIntervalMinutes = NormalizeBaiduHotSearchInterval(
normalized.BaiduHotSearchAutoRefreshIntervalMinutes);
normalized.BaiduHotSearchSourceType = BaiduHotSearchSourceTypes.Normalize(normalized.BaiduHotSearchSourceType);
normalized.WeatherAutoRefreshIntervalMinutes = NormalizeWeatherInterval(normalized.WeatherAutoRefreshIntervalMinutes);
normalized.Stcn24ForumAutoRefreshIntervalMinutes = NormalizeStcn24ForumInterval(normalized.Stcn24ForumAutoRefreshIntervalMinutes);
normalized.Stcn24ForumSourceType = Stcn24ForumSourceTypes.Normalize(normalized.Stcn24ForumSourceType);
return normalized;
}
private static ComponentSettingsDocumentSnapshot NormalizeDocument(ComponentSettingsDocumentSnapshot? snapshot)
{
var normalized = snapshot?.Clone() ?? new ComponentSettingsDocumentSnapshot();
normalized.DefaultSettings = NormalizeSnapshot(normalized.DefaultSettings);
var instanceSettings = new Dictionary<string, ComponentSettingsSnapshot>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in normalized.InstanceSettings)
{
var key = NormalizeInstanceKey(pair.Key);
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
instanceSettings[key] = NormalizeSnapshot(pair.Value);
}
var pluginSettings = new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in normalized.PluginSettings)
{
var key = NormalizeInstanceKey(pair.Key);
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
pluginSettings[key] = pair.Value.Clone();
}
normalized.InstanceSettings = instanceSettings;
normalized.PluginSettings = pluginSettings;
return normalized;
}
private static List<ImportedClassScheduleSnapshot> NormalizeImportedSchedules(
IReadOnlyList<ImportedClassScheduleSnapshot>? schedules)
{
if (schedules is null || schedules.Count == 0)
{
return [];
}
var result = new List<ImportedClassScheduleSnapshot>(schedules.Count);
var seenIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var schedule in schedules)
{
if (schedule is null)
{
continue;
}
var id = schedule.Id?.Trim() ?? string.Empty;
var filePath = schedule.FilePath?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(filePath))
{
continue;
}
if (!seenIds.Add(id))
{
continue;
}
result.Add(new ImportedClassScheduleSnapshot
{
Id = id,
DisplayName = schedule.DisplayName?.Trim() ?? string.Empty,
FilePath = filePath
});
}
return result;
}
private static string NormalizeActiveScheduleId(
string? activeScheduleId,
IReadOnlyList<ImportedClassScheduleSnapshot> schedules)
{
var activeId = activeScheduleId?.Trim() ?? string.Empty;
if (schedules.Count == 0)
{
return string.Empty;
}
if (string.IsNullOrWhiteSpace(activeId))
{
return schedules[0].Id;
}
return schedules.Any(item => string.Equals(item.Id, activeId, StringComparison.OrdinalIgnoreCase))
? activeId
: schedules[0].Id;
}
private static string NormalizeDesktopClockTimeZoneId(string? timeZoneId)
{
var normalizedId = string.IsNullOrWhiteSpace(timeZoneId)
? "China Standard Time"
: timeZoneId.Trim();
return WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal(normalizedId).Id;
}
private static int NormalizeCnrInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 60);
}
private static int NormalizeDailyWordInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 360);
}
private static int NormalizeIfengNewsInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 20);
}
private static int NormalizeBilibiliHotSearchInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 15);
}
private static int NormalizeBaiduHotSearchInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 15);
}
private static int NormalizeWeatherInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 12);
}
private static int NormalizeStcn24ForumInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 20);
}
private static string BuildInstanceKey(string componentId, string? placementId)
{
var normalizedComponentId = componentId?.Trim() ?? string.Empty;
var normalizedPlacementId = placementId?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(normalizedComponentId) || string.IsNullOrWhiteSpace(normalizedPlacementId))
{
return string.Empty;
}
return $"{normalizedComponentId}::{normalizedPlacementId}";
}
private static string NormalizeInstanceKey(string? key)
{
return key?.Trim() ?? string.Empty;
} }
private bool HasScopedComponentContext() private bool HasScopedComponentContext()
@@ -695,192 +228,4 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
return !string.IsNullOrWhiteSpace(_scopedComponentId) && return !string.IsNullOrWhiteSpace(_scopedComponentId) &&
!string.IsNullOrWhiteSpace(_scopedPlacementId); !string.IsNullOrWhiteSpace(_scopedPlacementId);
} }
private void UpdateCache(ComponentSettingsDocumentSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc)
{
_cachedPath = _settingsPath;
_cachedSnapshot = snapshot.Clone();
_cachedWriteTimeUtc = writeTimeUtc;
_lastProbeUtc = probeTimeUtc;
}
internal static void ResetCacheForTests()
{
lock (CacheGate)
{
_cachedPath = null;
_cachedSnapshot = null;
_cachedWriteTimeUtc = DateTime.MinValue;
_lastProbeUtc = DateTime.MinValue;
}
}
private void LogLoadDetails(
ComponentSettingsDocumentFormat format,
bool rewroteToCanonical,
ComponentSettingsDocumentSnapshot snapshot)
{
AppLogger.Info(
"ComponentSettings",
$"Loaded component settings document. Format={format}; RewroteToCanonical={rewroteToCanonical}; " +
$"InstanceSettings={snapshot.InstanceSettings.Count}; PluginSettings={snapshot.PluginSettings.Count}; Path={_settingsPath}");
}
private static bool TryGetDocumentFormat(
JsonElement rootElement,
out ComponentSettingsDocumentFormat format)
{
format = ComponentSettingsDocumentFormat.EmptyDocument;
if (rootElement.ValueKind != JsonValueKind.Object)
{
return false;
}
var hasDocumentProperties = false;
var requiresCanonicalRewrite = false;
foreach (var property in rootElement.EnumerateObject())
{
if (!IsDocumentPropertyName(property.Name))
{
continue;
}
hasDocumentProperties = true;
if (!IsCanonicalDocumentPropertyName(property.Name))
{
requiresCanonicalRewrite = true;
}
}
if (!hasDocumentProperties)
{
return false;
}
format = requiresCanonicalRewrite
? ComponentSettingsDocumentFormat.PascalCaseDocument
: ComponentSettingsDocumentFormat.CanonicalDocument;
return true;
}
private static bool IsDocumentPropertyName(string propertyName)
{
return string.Equals(propertyName, "defaultSettings", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "instanceSettings", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "pluginSettings", StringComparison.OrdinalIgnoreCase);
}
private static bool IsCanonicalDocumentPropertyName(string propertyName)
{
return string.Equals(propertyName, "defaultSettings", StringComparison.Ordinal) ||
string.Equals(propertyName, "instanceSettings", StringComparison.Ordinal) ||
string.Equals(propertyName, "pluginSettings", StringComparison.Ordinal);
}
private sealed class ComponentSettingsDocumentSnapshot
{
public ComponentSettingsSnapshot DefaultSettings { get; set; } = new();
public Dictionary<string, ComponentSettingsSnapshot> InstanceSettings { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, JsonElement> PluginSettings { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
public ComponentSettingsDocumentSnapshot Clone()
{
var clone = new ComponentSettingsDocumentSnapshot
{
DefaultSettings = DefaultSettings?.Clone() ?? new ComponentSettingsSnapshot(),
InstanceSettings = new Dictionary<string, ComponentSettingsSnapshot>(StringComparer.OrdinalIgnoreCase),
PluginSettings = new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase)
};
foreach (var pair in InstanceSettings)
{
clone.InstanceSettings[pair.Key] = pair.Value?.Clone() ?? new ComponentSettingsSnapshot();
}
foreach (var pair in PluginSettings)
{
clone.PluginSettings[pair.Key] = pair.Value.Clone();
}
return clone;
}
}
private sealed class LegacyComponentSettingsSnapshot
{
public string DailyArtworkMirrorSource { get; set; } = DailyArtworkMirrorSources.Overseas;
public List<ImportedClassScheduleSnapshot>? ImportedClassSchedules { get; set; }
public string? ActiveImportedClassScheduleId { get; set; }
public bool StudyEnvironmentShowDisplayDb { get; set; } = true;
public bool StudyEnvironmentShowDbfs { get; set; }
public string DesktopClockTimeZoneId { get; set; } = "China Standard Time";
public string DesktopClockSecondHandMode { get; set; } = "Tick";
public List<string>? WorldClockTimeZoneIds { get; set; }
public string WorldClockSecondHandMode { get; set; } = "Tick";
public bool CnrDailyNewsAutoRotateEnabled { get; set; } = true;
public int CnrDailyNewsAutoRotateIntervalMinutes { get; set; } = 60;
public bool IfengNewsAutoRefreshEnabled { get; set; } = true;
public int IfengNewsAutoRefreshIntervalMinutes { get; set; } = 20;
public string IfengNewsChannelType { get; set; } = IfengNewsChannelTypes.Comprehensive;
public bool DailyWordAutoRefreshEnabled { get; set; } = true;
public int DailyWordAutoRefreshIntervalMinutes { get; set; } = 360;
public bool BilibiliHotSearchAutoRefreshEnabled { get; set; } = true;
public int BilibiliHotSearchAutoRefreshIntervalMinutes { get; set; } = 15;
public bool BaiduHotSearchAutoRefreshEnabled { get; set; } = true;
public int BaiduHotSearchAutoRefreshIntervalMinutes { get; set; } = 15;
public string BaiduHotSearchSourceType { get; set; } = BaiduHotSearchSourceTypes.Official;
public bool WeatherAutoRefreshEnabled { get; set; } = true;
public int WeatherAutoRefreshIntervalMinutes { get; set; } = 12;
public bool Stcn24ForumAutoRefreshEnabled { get; set; } = true;
public int Stcn24ForumAutoRefreshIntervalMinutes { get; set; } = 20;
public string Stcn24ForumSourceType { get; set; } = Stcn24ForumSourceTypes.LatestCreated;
}
private readonly record struct ComponentSettingsLoadDetails(
ComponentSettingsDocumentSnapshot Snapshot,
ComponentSettingsDocumentFormat Format,
bool ShouldRewriteToCanonical)
{
public static ComponentSettingsLoadDetails Empty { get; } = new(
new ComponentSettingsDocumentSnapshot(),
ComponentSettingsDocumentFormat.EmptyDocument,
false);
}
private enum ComponentSettingsDocumentFormat
{
EmptyDocument,
CanonicalDocument,
PascalCaseDocument,
LegacySnapshot
}
} }

View File

@@ -10,6 +10,7 @@ using Avalonia.Media;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.ComponentSystem.Extensions; using LanMountainDesktop.ComponentSystem.Extensions;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Views.Components; using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Services; namespace LanMountainDesktop.Services;
@@ -114,6 +115,11 @@ public static class DesktopComponentRegistryFactory
{ {
try try
{ {
var settingsService = contribution.Plugin.Services.GetService(typeof(ISettingsService)) as ISettingsService
?? HostSettingsFacadeProvider.GetOrCreate().Settings;
var pluginSettings = new PluginScopedSettingsService(
contribution.Plugin.Manifest.Id,
settingsService);
var pluginContext = new PluginDesktopComponentContext( var pluginContext = new PluginDesktopComponentContext(
contribution.Plugin.Manifest, contribution.Plugin.Manifest,
contribution.Plugin.Context.PluginDirectory, contribution.Plugin.Context.PluginDirectory,
@@ -122,7 +128,8 @@ public static class DesktopComponentRegistryFactory
contribution.Plugin.Context.Properties, contribution.Plugin.Context.Properties,
contribution.Registration.ComponentId, contribution.Registration.ComponentId,
context.PlacementId, context.PlacementId,
context.CellSize); context.CellSize,
pluginSettings);
return contribution.Registration.ControlFactory(contribution.Plugin.Services, pluginContext); return contribution.Registration.ControlFactory(contribution.Plugin.Services, pluginContext);
} }

View File

@@ -1,251 +1,19 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services; namespace LanMountainDesktop.Services;
public sealed class DesktopLayoutSettingsService public sealed class DesktopLayoutSettingsService
{ {
private static readonly JsonSerializerOptions SerializerOptions = new() private readonly IComponentLayoutStore _layoutStore = ComponentDomainStorageProvider.Instance;
{
WriteIndented = true
};
private static readonly object CacheGate = new();
private static readonly TimeSpan CacheProbeInterval = TimeSpan.FromMilliseconds(400);
private static string? _cachedPath;
private static DesktopLayoutSettingsSnapshot? _cachedSnapshot;
private static DateTime _cachedWriteTimeUtc = DateTime.MinValue;
private static DateTime _lastProbeUtc = DateTime.MinValue;
private readonly string _settingsPath;
private readonly string _legacyAppSettingsPath;
public DesktopLayoutSettingsService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var settingsDirectory = Path.Combine(appData, "LanMountainDesktop");
_settingsPath = Path.Combine(settingsDirectory, "desktop-layout-settings.json");
_legacyAppSettingsPath = Path.Combine(settingsDirectory, "settings.json");
}
public DesktopLayoutSettingsSnapshot Load() public DesktopLayoutSettingsSnapshot Load()
{ {
try return _layoutStore.LoadLayout();
{
lock (CacheGate)
{
var nowUtc = DateTime.UtcNow;
if (TryGetCachedWithoutProbe(nowUtc, out var cached))
{
return cached;
}
var hasFile = File.Exists(_settingsPath);
var writeTimeUtc = hasFile
? File.GetLastWriteTimeUtc(_settingsPath)
: DateTime.MinValue;
_lastProbeUtc = nowUtc;
if (TryGetCachedAfterProbe(writeTimeUtc, out cached))
{
return cached;
}
DesktopLayoutSettingsSnapshot loadedSnapshot;
var loadedFromLegacy = false;
if (hasFile)
{
loadedSnapshot = LoadSnapshotFromDisk();
}
else if (TryLoadLegacySnapshot(out var migratedSnapshot))
{
loadedSnapshot = migratedSnapshot;
loadedFromLegacy = true;
}
else
{
loadedSnapshot = new DesktopLayoutSettingsSnapshot();
}
var normalizedSnapshot = NormalizeSnapshot(loadedSnapshot);
if (loadedFromLegacy)
{
writeTimeUtc = PersistSnapshotToDisk(normalizedSnapshot);
}
UpdateCache(normalizedSnapshot, writeTimeUtc, nowUtc);
return normalizedSnapshot.Clone();
}
}
catch (Exception ex)
{
AppLogger.Warn("DesktopLayout", $"Failed to load desktop layout settings from '{_settingsPath}'.", ex);
return new DesktopLayoutSettingsSnapshot();
}
} }
public void Save(DesktopLayoutSettingsSnapshot snapshot) public void Save(DesktopLayoutSettingsSnapshot snapshot)
{ {
var snapshotToPersist = NormalizeSnapshot(snapshot); _layoutStore.SaveLayout(snapshot ?? new DesktopLayoutSettingsSnapshot());
try
{
var writeTimeUtc = PersistSnapshotToDisk(snapshotToPersist);
lock (CacheGate)
{
UpdateCache(snapshotToPersist, writeTimeUtc, DateTime.UtcNow);
}
}
catch (Exception ex)
{
AppLogger.Warn("DesktopLayout", $"Failed to save desktop layout settings to '{_settingsPath}'.", ex);
}
}
private bool TryGetCachedWithoutProbe(DateTime nowUtc, out DesktopLayoutSettingsSnapshot snapshot)
{
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) &&
_cachedSnapshot is not null &&
nowUtc - _lastProbeUtc < CacheProbeInterval)
{
snapshot = _cachedSnapshot.Clone();
return true;
}
snapshot = null!;
return false;
}
private bool TryGetCachedAfterProbe(DateTime writeTimeUtc, out DesktopLayoutSettingsSnapshot snapshot)
{
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) &&
_cachedSnapshot is not null &&
writeTimeUtc == _cachedWriteTimeUtc)
{
snapshot = _cachedSnapshot.Clone();
return true;
}
snapshot = null!;
return false;
}
private DesktopLayoutSettingsSnapshot LoadSnapshotFromDisk()
{
try
{
var json = File.ReadAllText(_settingsPath);
var snapshot = JsonSerializer.Deserialize<DesktopLayoutSettingsSnapshot>(json, SerializerOptions);
return NormalizeSnapshot(snapshot);
}
catch (Exception ex)
{
AppLogger.Warn("DesktopLayout", $"Failed to deserialize desktop layout settings from '{_settingsPath}'.", ex);
return new DesktopLayoutSettingsSnapshot();
}
}
private bool TryLoadLegacySnapshot(out DesktopLayoutSettingsSnapshot snapshot)
{
snapshot = new DesktopLayoutSettingsSnapshot();
try
{
if (!File.Exists(_legacyAppSettingsPath))
{
return false;
}
var legacyJson = File.ReadAllText(_legacyAppSettingsPath);
var legacy = JsonSerializer.Deserialize<LegacyDesktopLayoutSettingsSnapshot>(legacyJson, SerializerOptions);
if (legacy is null)
{
return false;
}
snapshot = new DesktopLayoutSettingsSnapshot
{
DesktopPageCount = legacy.DesktopPageCount,
CurrentDesktopSurfaceIndex = legacy.CurrentDesktopSurfaceIndex,
DesktopComponentPlacements = legacy.DesktopComponentPlacements ?? []
};
return true;
}
catch (Exception ex)
{
AppLogger.Warn("DesktopLayout", $"Failed to migrate legacy desktop layout settings from '{_legacyAppSettingsPath}'.", ex);
return false;
}
}
private DateTime PersistSnapshotToDisk(DesktopLayoutSettingsSnapshot snapshot)
{
var directory = Path.GetDirectoryName(_settingsPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(snapshot, SerializerOptions);
File.WriteAllText(_settingsPath, json);
return File.Exists(_settingsPath)
? File.GetLastWriteTimeUtc(_settingsPath)
: DateTime.UtcNow;
}
private static DesktopLayoutSettingsSnapshot NormalizeSnapshot(DesktopLayoutSettingsSnapshot? snapshot)
{
var normalized = snapshot?.Clone() ?? new DesktopLayoutSettingsSnapshot();
normalized.DesktopPageCount = Math.Max(1, normalized.DesktopPageCount);
normalized.CurrentDesktopSurfaceIndex = Math.Max(0, normalized.CurrentDesktopSurfaceIndex);
var placements = new List<DesktopComponentPlacementSnapshot>(normalized.DesktopComponentPlacements?.Count ?? 0);
if (normalized.DesktopComponentPlacements is not null)
{
foreach (var placement in normalized.DesktopComponentPlacements)
{
if (placement is null)
{
continue;
}
placements.Add(new DesktopComponentPlacementSnapshot
{
PlacementId = placement.PlacementId?.Trim() ?? string.Empty,
PageIndex = Math.Max(0, placement.PageIndex),
ComponentId = placement.ComponentId?.Trim() ?? string.Empty,
Row = Math.Max(0, placement.Row),
Column = Math.Max(0, placement.Column),
WidthCells = Math.Max(1, placement.WidthCells),
HeightCells = Math.Max(1, placement.HeightCells)
});
}
}
normalized.DesktopComponentPlacements = placements;
return normalized;
}
private void UpdateCache(DesktopLayoutSettingsSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc)
{
_cachedPath = _settingsPath;
_cachedSnapshot = snapshot.Clone();
_cachedWriteTimeUtc = writeTimeUtc;
_lastProbeUtc = probeTimeUtc;
}
private sealed class LegacyDesktopLayoutSettingsSnapshot
{
public int DesktopPageCount { get; set; } = 1;
public int CurrentDesktopSurfaceIndex { get; set; }
public List<DesktopComponentPlacementSnapshot>? DesktopComponentPlacements { get; set; }
} }
} }

View File

@@ -0,0 +1,845 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using LanMountainDesktop.Models;
using Microsoft.Data.Sqlite;
namespace LanMountainDesktop.Services.Settings;
public interface IComponentLayoutStore
{
DesktopLayoutSettingsSnapshot LoadLayout();
void SaveLayout(DesktopLayoutSettingsSnapshot snapshot);
}
public interface IComponentStateStore
{
ComponentSettingsSnapshot LoadState(string componentId, string? placementId);
void SaveState(string componentId, string? placementId, ComponentSettingsSnapshot snapshot);
void DeleteState(string componentId, string? placementId);
}
public interface IComponentMessageStore
{
T LoadSection<T>(string componentId, string? placementId, string sectionId) where T : new();
void SaveSection<T>(string componentId, string? placementId, string sectionId, T section);
void DeleteSection(string componentId, string? placementId, string sectionId);
}
internal static class ComponentDomainStorageProvider
{
private static readonly object Gate = new();
private static SqliteComponentDomainStorage? _instance;
public static SqliteComponentDomainStorage Instance
{
get
{
lock (Gate)
{
_instance ??= new SqliteComponentDomainStorage();
return _instance;
}
}
}
}
internal sealed class SqliteComponentDomainStorage :
IComponentLayoutStore,
IComponentStateStore,
IComponentMessageStore
{
private const string MigrationMarkerKey = "component_domain_v1";
private const string DefaultInstanceKey = "__default__";
private const string LegacySectionId = "__legacy__";
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
private readonly object _gate = new();
private readonly string _settingsRoot;
private readonly string _dbPath;
private readonly string _layoutJsonPath;
private readonly string _componentJsonPath;
public SqliteComponentDomainStorage(string? settingsRoot = null)
{
_settingsRoot = string.IsNullOrWhiteSpace(settingsRoot)
? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop")
: settingsRoot.Trim();
_dbPath = Path.Combine(_settingsRoot, "component-state.db");
_layoutJsonPath = Path.Combine(_settingsRoot, "desktop-layout-settings.json");
_componentJsonPath = Path.Combine(_settingsRoot, "component-settings.json");
Directory.CreateDirectory(_settingsRoot);
InitializeDatabase();
}
public DesktopLayoutSettingsSnapshot LoadLayout()
{
lock (_gate)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT desktop_page_count, current_desktop_surface_index
FROM component_layout
WHERE id = 1;
""";
using var reader = command.ExecuteReader();
if (!reader.Read())
{
return new DesktopLayoutSettingsSnapshot();
}
return new DesktopLayoutSettingsSnapshot
{
DesktopPageCount = Math.Max(1, reader.GetInt32(0)),
CurrentDesktopSurfaceIndex = Math.Max(0, reader.GetInt32(1)),
DesktopComponentPlacements = LoadPlacements(connection)
};
}
}
public void SaveLayout(DesktopLayoutSettingsSnapshot snapshot)
{
var normalized = snapshot?.Clone() ?? new DesktopLayoutSettingsSnapshot();
normalized.DesktopPageCount = Math.Max(1, normalized.DesktopPageCount);
normalized.CurrentDesktopSurfaceIndex = Math.Max(0, normalized.CurrentDesktopSurfaceIndex);
lock (_gate)
{
using var connection = OpenConnection();
using var transaction = connection.BeginTransaction();
using (var command = connection.CreateCommand())
{
command.Transaction = transaction;
command.CommandText = """
INSERT INTO component_layout(id, desktop_page_count, current_desktop_surface_index, updated_utc)
VALUES(1, $count, $index, $updated)
ON CONFLICT(id) DO UPDATE SET
desktop_page_count = excluded.desktop_page_count,
current_desktop_surface_index = excluded.current_desktop_surface_index,
updated_utc = excluded.updated_utc;
""";
command.Parameters.AddWithValue("$count", normalized.DesktopPageCount);
command.Parameters.AddWithValue("$index", normalized.CurrentDesktopSurfaceIndex);
command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
command.ExecuteNonQuery();
}
using (var deleteCommand = connection.CreateCommand())
{
deleteCommand.Transaction = transaction;
deleteCommand.CommandText = "DELETE FROM component_placement;";
deleteCommand.ExecuteNonQuery();
}
if (normalized.DesktopComponentPlacements is { Count: > 0 })
{
foreach (var placement in normalized.DesktopComponentPlacements)
{
if (placement is null || string.IsNullOrWhiteSpace(placement.PlacementId))
{
continue;
}
using var insertCommand = connection.CreateCommand();
insertCommand.Transaction = transaction;
insertCommand.CommandText = """
INSERT INTO component_placement(
placement_id, page_index, component_id, row_index, column_index, width_cells, height_cells, updated_utc)
VALUES($placementId, $page, $componentId, $row, $column, $width, $height, $updated);
""";
insertCommand.Parameters.AddWithValue("$placementId", placement.PlacementId.Trim());
insertCommand.Parameters.AddWithValue("$page", Math.Max(0, placement.PageIndex));
insertCommand.Parameters.AddWithValue("$componentId", placement.ComponentId?.Trim() ?? string.Empty);
insertCommand.Parameters.AddWithValue("$row", Math.Max(0, placement.Row));
insertCommand.Parameters.AddWithValue("$column", Math.Max(0, placement.Column));
insertCommand.Parameters.AddWithValue("$width", Math.Max(1, placement.WidthCells));
insertCommand.Parameters.AddWithValue("$height", Math.Max(1, placement.HeightCells));
insertCommand.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
insertCommand.ExecuteNonQuery();
}
}
transaction.Commit();
}
}
public ComponentSettingsSnapshot LoadState(string componentId, string? placementId)
{
var instanceKey = BuildInstanceKey(componentId, placementId);
lock (_gate)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT state_json
FROM component_state
WHERE instance_key = $instanceKey
LIMIT 1;
""";
command.Parameters.AddWithValue("$instanceKey", instanceKey);
var json = command.ExecuteScalar() as string;
if (string.IsNullOrWhiteSpace(json))
{
if (string.Equals(instanceKey, DefaultInstanceKey, StringComparison.OrdinalIgnoreCase))
{
return new ComponentSettingsSnapshot();
}
return LoadDefaultState(connection);
}
return DeserializeState(json);
}
}
public void SaveState(string componentId, string? placementId, ComponentSettingsSnapshot snapshot)
{
var instanceKey = BuildInstanceKey(componentId, placementId);
var normalizedComponentId = NormalizeKey(componentId);
var normalizedPlacementId = NormalizePlacement(placementId);
var json = JsonSerializer.Serialize(snapshot ?? new ComponentSettingsSnapshot(), SerializerOptions);
lock (_gate)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO component_state(instance_key, component_id, placement_id, state_json, updated_utc)
VALUES($instanceKey, $componentId, $placementId, $stateJson, $updated)
ON CONFLICT(instance_key) DO UPDATE SET
component_id = excluded.component_id,
placement_id = excluded.placement_id,
state_json = excluded.state_json,
updated_utc = excluded.updated_utc;
""";
command.Parameters.AddWithValue("$instanceKey", instanceKey);
command.Parameters.AddWithValue("$componentId", normalizedComponentId);
command.Parameters.AddWithValue("$placementId", normalizedPlacementId);
command.Parameters.AddWithValue("$stateJson", json);
command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
command.ExecuteNonQuery();
}
}
public void DeleteState(string componentId, string? placementId)
{
var instanceKey = BuildInstanceKey(componentId, placementId);
if (string.Equals(instanceKey, DefaultInstanceKey, StringComparison.OrdinalIgnoreCase))
{
return;
}
lock (_gate)
{
using var connection = OpenConnection();
using var transaction = connection.BeginTransaction();
using (var stateDelete = connection.CreateCommand())
{
stateDelete.Transaction = transaction;
stateDelete.CommandText = "DELETE FROM component_state WHERE instance_key = $instanceKey;";
stateDelete.Parameters.AddWithValue("$instanceKey", instanceKey);
stateDelete.ExecuteNonQuery();
}
using (var messageDelete = connection.CreateCommand())
{
messageDelete.Transaction = transaction;
messageDelete.CommandText = "DELETE FROM component_message WHERE instance_key = $instanceKey;";
messageDelete.Parameters.AddWithValue("$instanceKey", instanceKey);
messageDelete.ExecuteNonQuery();
}
transaction.Commit();
}
}
public T LoadSection<T>(string componentId, string? placementId, string sectionId) where T : new()
{
var instanceKey = BuildInstanceKey(componentId, placementId);
var normalizedSectionId = NormalizeSection(sectionId);
lock (_gate)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT message_json
FROM component_message
WHERE instance_key = $instanceKey
AND section_id = $sectionId
LIMIT 1;
""";
command.Parameters.AddWithValue("$instanceKey", instanceKey);
command.Parameters.AddWithValue("$sectionId", normalizedSectionId);
var json = command.ExecuteScalar() as string;
if (string.IsNullOrWhiteSpace(json))
{
return new T();
}
try
{
return JsonSerializer.Deserialize<T>(json, SerializerOptions) ?? new T();
}
catch
{
return new T();
}
}
}
public void SaveSection<T>(string componentId, string? placementId, string sectionId, T section)
{
var instanceKey = BuildInstanceKey(componentId, placementId);
var normalizedComponentId = NormalizeKey(componentId);
var normalizedPlacementId = NormalizePlacement(placementId);
var normalizedSectionId = NormalizeSection(sectionId);
var json = JsonSerializer.Serialize(section, SerializerOptions);
lock (_gate)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO component_message(instance_key, component_id, placement_id, section_id, message_json, updated_utc)
VALUES($instanceKey, $componentId, $placementId, $sectionId, $messageJson, $updated)
ON CONFLICT(instance_key, section_id) DO UPDATE SET
component_id = excluded.component_id,
placement_id = excluded.placement_id,
message_json = excluded.message_json,
updated_utc = excluded.updated_utc;
""";
command.Parameters.AddWithValue("$instanceKey", instanceKey);
command.Parameters.AddWithValue("$componentId", normalizedComponentId);
command.Parameters.AddWithValue("$placementId", normalizedPlacementId);
command.Parameters.AddWithValue("$sectionId", normalizedSectionId);
command.Parameters.AddWithValue("$messageJson", json);
command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
command.ExecuteNonQuery();
}
}
public void DeleteSection(string componentId, string? placementId, string sectionId)
{
var instanceKey = BuildInstanceKey(componentId, placementId);
var normalizedSectionId = NormalizeSection(sectionId);
lock (_gate)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
DELETE FROM component_message
WHERE instance_key = $instanceKey
AND section_id = $sectionId;
""";
command.Parameters.AddWithValue("$instanceKey", instanceKey);
command.Parameters.AddWithValue("$sectionId", normalizedSectionId);
command.ExecuteNonQuery();
}
}
public T LoadLegacyMessage<T>(string componentId, string? placementId) where T : new()
{
return LoadSection<T>(componentId, placementId, LegacySectionId);
}
public void SaveLegacyMessage<T>(string componentId, string? placementId, T section)
{
SaveSection(componentId, placementId, LegacySectionId, section);
}
public void DeleteLegacyMessage(string componentId, string? placementId)
{
DeleteSection(componentId, placementId, LegacySectionId);
}
private void InitializeDatabase()
{
lock (_gate)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
CREATE TABLE IF NOT EXISTS settings_meta(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS component_layout(
id INTEGER PRIMARY KEY CHECK(id = 1),
desktop_page_count INTEGER NOT NULL,
current_desktop_surface_index INTEGER NOT NULL,
updated_utc TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS component_placement(
placement_id TEXT PRIMARY KEY,
page_index INTEGER NOT NULL,
component_id TEXT NOT NULL,
row_index INTEGER NOT NULL,
column_index INTEGER NOT NULL,
width_cells INTEGER NOT NULL,
height_cells INTEGER NOT NULL,
updated_utc TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS component_state(
instance_key TEXT PRIMARY KEY,
component_id TEXT NOT NULL,
placement_id TEXT NOT NULL,
state_json TEXT NOT NULL,
updated_utc TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS component_message(
instance_key TEXT NOT NULL,
component_id TEXT NOT NULL,
placement_id TEXT NOT NULL,
section_id TEXT NOT NULL,
message_json TEXT NOT NULL,
updated_utc TEXT NOT NULL,
PRIMARY KEY(instance_key, section_id)
);
""";
command.ExecuteNonQuery();
if (!IsMigrationApplied(connection))
{
ApplyInitialMigration(connection);
}
}
}
private bool IsMigrationApplied(SqliteConnection connection)
{
using var command = connection.CreateCommand();
command.CommandText = """
SELECT value
FROM settings_meta
WHERE key = $key
LIMIT 1;
""";
command.Parameters.AddWithValue("$key", MigrationMarkerKey);
var raw = command.ExecuteScalar() as string;
return string.Equals(raw, "applied", StringComparison.OrdinalIgnoreCase);
}
private void ApplyInitialMigration(SqliteConnection connection)
{
AppLogger.Info("ComponentDomainStorage", "Starting one-shot migration from legacy JSON files to SQLite.");
using var transaction = connection.BeginTransaction();
try
{
if (TryLoadLegacyLayout(out var layout))
{
PersistLayout(connection, transaction, layout);
}
if (TryLoadLegacyComponentDocument(out var document))
{
PersistComponentDocument(connection, transaction, document);
}
using var markerCommand = connection.CreateCommand();
markerCommand.Transaction = transaction;
markerCommand.CommandText = """
INSERT INTO settings_meta(key, value)
VALUES($key, 'applied')
ON CONFLICT(key) DO UPDATE SET value = 'applied';
""";
markerCommand.Parameters.AddWithValue("$key", MigrationMarkerKey);
markerCommand.ExecuteNonQuery();
transaction.Commit();
BackupLegacyFile(_layoutJsonPath);
BackupLegacyFile(_componentJsonPath);
AppLogger.Info("ComponentDomainStorage", "Legacy JSON migration completed.");
}
catch (Exception ex)
{
transaction.Rollback();
AppLogger.Error("ComponentDomainStorage", "Legacy JSON migration failed. SQLite writes are blocked for this session.", ex);
throw;
}
}
private void PersistLayout(
SqliteConnection connection,
SqliteTransaction transaction,
DesktopLayoutSettingsSnapshot snapshot)
{
using (var command = connection.CreateCommand())
{
command.Transaction = transaction;
command.CommandText = """
INSERT INTO component_layout(id, desktop_page_count, current_desktop_surface_index, updated_utc)
VALUES(1, $count, $index, $updated)
ON CONFLICT(id) DO UPDATE SET
desktop_page_count = excluded.desktop_page_count,
current_desktop_surface_index = excluded.current_desktop_surface_index,
updated_utc = excluded.updated_utc;
""";
command.Parameters.AddWithValue("$count", Math.Max(1, snapshot.DesktopPageCount));
command.Parameters.AddWithValue("$index", Math.Max(0, snapshot.CurrentDesktopSurfaceIndex));
command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
command.ExecuteNonQuery();
}
if (snapshot.DesktopComponentPlacements is not { Count: > 0 })
{
return;
}
foreach (var placement in snapshot.DesktopComponentPlacements)
{
if (placement is null || string.IsNullOrWhiteSpace(placement.PlacementId))
{
continue;
}
using var placementCommand = connection.CreateCommand();
placementCommand.Transaction = transaction;
placementCommand.CommandText = """
INSERT INTO component_placement(
placement_id, page_index, component_id, row_index, column_index, width_cells, height_cells, updated_utc)
VALUES($placementId, $page, $componentId, $row, $column, $width, $height, $updated)
ON CONFLICT(placement_id) DO UPDATE SET
page_index = excluded.page_index,
component_id = excluded.component_id,
row_index = excluded.row_index,
column_index = excluded.column_index,
width_cells = excluded.width_cells,
height_cells = excluded.height_cells,
updated_utc = excluded.updated_utc;
""";
placementCommand.Parameters.AddWithValue("$placementId", placement.PlacementId.Trim());
placementCommand.Parameters.AddWithValue("$page", Math.Max(0, placement.PageIndex));
placementCommand.Parameters.AddWithValue("$componentId", placement.ComponentId?.Trim() ?? string.Empty);
placementCommand.Parameters.AddWithValue("$row", Math.Max(0, placement.Row));
placementCommand.Parameters.AddWithValue("$column", Math.Max(0, placement.Column));
placementCommand.Parameters.AddWithValue("$width", Math.Max(1, placement.WidthCells));
placementCommand.Parameters.AddWithValue("$height", Math.Max(1, placement.HeightCells));
placementCommand.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
placementCommand.ExecuteNonQuery();
}
}
private void PersistComponentDocument(
SqliteConnection connection,
SqliteTransaction transaction,
LegacyComponentDocument document)
{
PersistComponentState(connection, transaction, DefaultInstanceKey, "__default__", string.Empty, document.DefaultSettings ?? new ComponentSettingsSnapshot());
if (document.InstanceSettings is not null)
{
foreach (var pair in document.InstanceSettings)
{
if (!TrySplitInstanceKey(pair.Key, out var componentId, out var placementId))
{
continue;
}
PersistComponentState(connection, transaction, pair.Key.Trim(), componentId, placementId, pair.Value ?? new ComponentSettingsSnapshot());
}
}
if (document.PluginSettings is null)
{
return;
}
foreach (var pair in document.PluginSettings)
{
if (!TrySplitInstanceKey(pair.Key, out var componentId, out var placementId))
{
continue;
}
using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = """
INSERT INTO component_message(instance_key, component_id, placement_id, section_id, message_json, updated_utc)
VALUES($instanceKey, $componentId, $placementId, $sectionId, $json, $updated)
ON CONFLICT(instance_key, section_id) DO UPDATE SET
component_id = excluded.component_id,
placement_id = excluded.placement_id,
message_json = excluded.message_json,
updated_utc = excluded.updated_utc;
""";
command.Parameters.AddWithValue("$instanceKey", pair.Key.Trim());
command.Parameters.AddWithValue("$componentId", componentId);
command.Parameters.AddWithValue("$placementId", placementId);
command.Parameters.AddWithValue("$sectionId", LegacySectionId);
command.Parameters.AddWithValue("$json", pair.Value.GetRawText());
command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
command.ExecuteNonQuery();
}
}
private static void PersistComponentState(
SqliteConnection connection,
SqliteTransaction transaction,
string instanceKey,
string componentId,
string placementId,
ComponentSettingsSnapshot snapshot)
{
var json = JsonSerializer.Serialize(snapshot ?? new ComponentSettingsSnapshot(), SerializerOptions);
using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = """
INSERT INTO component_state(instance_key, component_id, placement_id, state_json, updated_utc)
VALUES($instanceKey, $componentId, $placementId, $stateJson, $updated)
ON CONFLICT(instance_key) DO UPDATE SET
component_id = excluded.component_id,
placement_id = excluded.placement_id,
state_json = excluded.state_json,
updated_utc = excluded.updated_utc;
""";
command.Parameters.AddWithValue("$instanceKey", instanceKey);
command.Parameters.AddWithValue("$componentId", componentId);
command.Parameters.AddWithValue("$placementId", placementId);
command.Parameters.AddWithValue("$stateJson", json);
command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
command.ExecuteNonQuery();
}
private bool TryLoadLegacyLayout(out DesktopLayoutSettingsSnapshot snapshot)
{
snapshot = new DesktopLayoutSettingsSnapshot();
if (!File.Exists(_layoutJsonPath))
{
return false;
}
try
{
var json = File.ReadAllText(_layoutJsonPath);
snapshot = JsonSerializer.Deserialize<DesktopLayoutSettingsSnapshot>(json, SerializerOptions) ?? new DesktopLayoutSettingsSnapshot();
return true;
}
catch (Exception ex)
{
AppLogger.Warn("ComponentDomainStorage", $"Failed to read legacy layout file '{_layoutJsonPath}'.", ex);
return false;
}
}
private bool TryLoadLegacyComponentDocument(out LegacyComponentDocument document)
{
document = new LegacyComponentDocument();
if (!File.Exists(_componentJsonPath))
{
return false;
}
try
{
var json = File.ReadAllText(_componentJsonPath);
using var parsed = JsonDocument.Parse(json);
if (parsed.RootElement.ValueKind != JsonValueKind.Object)
{
return false;
}
var hasDocumentShape = false;
foreach (var property in parsed.RootElement.EnumerateObject())
{
if (string.Equals(property.Name, "defaultSettings", StringComparison.OrdinalIgnoreCase) ||
string.Equals(property.Name, "instanceSettings", StringComparison.OrdinalIgnoreCase) ||
string.Equals(property.Name, "pluginSettings", StringComparison.OrdinalIgnoreCase))
{
hasDocumentShape = true;
break;
}
}
if (hasDocumentShape)
{
document = JsonSerializer.Deserialize<LegacyComponentDocument>(json, SerializerOptions) ?? new LegacyComponentDocument();
document.DefaultSettings ??= new ComponentSettingsSnapshot();
document.InstanceSettings ??= new Dictionary<string, ComponentSettingsSnapshot>(StringComparer.OrdinalIgnoreCase);
document.PluginSettings ??= new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
return true;
}
var legacySingle = JsonSerializer.Deserialize<ComponentSettingsSnapshot>(json, SerializerOptions) ?? new ComponentSettingsSnapshot();
document = new LegacyComponentDocument
{
DefaultSettings = legacySingle
};
return true;
}
catch (Exception ex)
{
AppLogger.Warn("ComponentDomainStorage", $"Failed to read legacy component settings file '{_componentJsonPath}'.", ex);
return false;
}
}
private static void BackupLegacyFile(string path)
{
if (!File.Exists(path))
{
return;
}
try
{
var backupPath = $"{path}.migrated.bak";
if (File.Exists(backupPath))
{
File.Delete(backupPath);
}
File.Move(path, backupPath);
}
catch (Exception ex)
{
AppLogger.Warn("ComponentDomainStorage", $"Failed to backup migrated legacy file '{path}'.", ex);
}
}
private static bool TrySplitInstanceKey(string key, out string componentId, out string placementId)
{
componentId = string.Empty;
placementId = string.Empty;
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
var normalized = key.Trim();
var parts = normalized.Split("::", 2, StringSplitOptions.TrimEntries);
if (parts.Length != 2 ||
string.IsNullOrWhiteSpace(parts[0]) ||
string.IsNullOrWhiteSpace(parts[1]))
{
return false;
}
componentId = parts[0];
placementId = parts[1];
return true;
}
private static string BuildInstanceKey(string componentId, string? placementId)
{
var normalizedComponentId = NormalizeKey(componentId);
var normalizedPlacementId = NormalizePlacement(placementId);
if (string.IsNullOrWhiteSpace(normalizedComponentId) ||
string.IsNullOrWhiteSpace(normalizedPlacementId))
{
return DefaultInstanceKey;
}
return $"{normalizedComponentId}::{normalizedPlacementId}";
}
private static string NormalizeKey(string? key)
{
return key?.Trim() ?? string.Empty;
}
private static string NormalizePlacement(string? placementId)
{
return placementId?.Trim() ?? string.Empty;
}
private static string NormalizeSection(string? sectionId)
{
return string.IsNullOrWhiteSpace(sectionId) ? LegacySectionId : sectionId.Trim();
}
private static ComponentSettingsSnapshot DeserializeState(string json)
{
try
{
return JsonSerializer.Deserialize<ComponentSettingsSnapshot>(json, SerializerOptions) ?? new ComponentSettingsSnapshot();
}
catch
{
return new ComponentSettingsSnapshot();
}
}
private static ComponentSettingsSnapshot LoadDefaultState(SqliteConnection connection)
{
using var command = connection.CreateCommand();
command.CommandText = """
SELECT state_json
FROM component_state
WHERE instance_key = $instanceKey
LIMIT 1;
""";
command.Parameters.AddWithValue("$instanceKey", DefaultInstanceKey);
var json = command.ExecuteScalar() as string;
return string.IsNullOrWhiteSpace(json) ? new ComponentSettingsSnapshot() : DeserializeState(json);
}
private static List<DesktopComponentPlacementSnapshot> LoadPlacements(SqliteConnection connection)
{
var placements = new List<DesktopComponentPlacementSnapshot>();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT placement_id, page_index, component_id, row_index, column_index, width_cells, height_cells
FROM component_placement
ORDER BY page_index, row_index, column_index;
""";
using var reader = command.ExecuteReader();
while (reader.Read())
{
placements.Add(new DesktopComponentPlacementSnapshot
{
PlacementId = reader.IsDBNull(0) ? string.Empty : reader.GetString(0),
PageIndex = reader.IsDBNull(1) ? 0 : Math.Max(0, reader.GetInt32(1)),
ComponentId = reader.IsDBNull(2) ? string.Empty : reader.GetString(2),
Row = reader.IsDBNull(3) ? 0 : Math.Max(0, reader.GetInt32(3)),
Column = reader.IsDBNull(4) ? 0 : Math.Max(0, reader.GetInt32(4)),
WidthCells = reader.IsDBNull(5) ? 1 : Math.Max(1, reader.GetInt32(5)),
HeightCells = reader.IsDBNull(6) ? 1 : Math.Max(1, reader.GetInt32(6))
});
}
return placements;
}
private SqliteConnection OpenConnection()
{
var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadWriteCreate;Cache=Shared");
connection.Open();
return connection;
}
private sealed class LegacyComponentDocument
{
public ComponentSettingsSnapshot? DefaultSettings { get; set; } = new();
public Dictionary<string, ComponentSettingsSnapshot>? InstanceSettings { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, JsonElement>? PluginSettings { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,28 @@
using System;
namespace LanMountainDesktop.Services.Settings;
internal static class HostSettingsFacadeProvider
{
private static readonly object Gate = new();
private static SettingsFacadeService? _instance;
public static ISettingsFacadeService GetOrCreate()
{
lock (Gate)
{
_instance ??= new SettingsFacadeService();
return _instance;
}
}
public static void BindPluginRuntime(PluginRuntimeService pluginRuntimeService)
{
ArgumentNullException.ThrowIfNull(pluginRuntimeService);
lock (Gate)
{
_instance ??= new SettingsFacadeService(pluginRuntimeService);
_instance.BindPluginRuntime(pluginRuntimeService);
}
}
}

View File

@@ -74,6 +74,15 @@ public interface IGridSettingsService
{ {
GridSettingsState Get(); GridSettingsState Get();
void Save(GridSettingsState state); void Save(GridSettingsState state);
string NormalizeSpacingPreset(string? value);
double ResolveGapRatio(string? preset);
double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent);
DesktopGridMetrics CalculateGridMetrics(
double hostWidth,
double hostHeight,
int shortSideCells,
double gapRatio,
double edgeInsetPx);
} }
public interface IWallpaperSettingsService public interface IWallpaperSettingsService
@@ -117,12 +126,14 @@ public interface IWeatherSettingsService
{ {
WeatherSettingsState Get(); WeatherSettingsState Get();
void Save(WeatherSettingsState state); void Save(WeatherSettingsState state);
IWeatherInfoService GetWeatherInfoService();
} }
public interface IRegionSettingsService public interface IRegionSettingsService
{ {
RegionSettingsState Get(); RegionSettingsState Get();
void Save(RegionSettingsState state); void Save(RegionSettingsState state);
TimeZoneService GetTimeZoneService();
} }
public interface IUpdateSettingsService public interface IUpdateSettingsService

View File

@@ -14,6 +14,7 @@ namespace LanMountainDesktop.Services.Settings;
internal sealed class GridSettingsService : IGridSettingsService internal sealed class GridSettingsService : IGridSettingsService
{ {
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly DesktopGridLayoutService _gridLayoutService = new();
public GridSettingsState Get() public GridSettingsState Get()
{ {
@@ -32,6 +33,36 @@ internal sealed class GridSettingsService : IGridSettingsService
snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent; snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent;
_appSettingsService.Save(snapshot); _appSettingsService.Save(snapshot);
} }
public string NormalizeSpacingPreset(string? value)
{
return _gridLayoutService.NormalizeSpacingPreset(value);
}
public double ResolveGapRatio(string? preset)
{
return _gridLayoutService.ResolveGapRatio(preset);
}
public double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent)
{
return _gridLayoutService.CalculateEdgeInset(hostWidth, hostHeight, shortSideCells, insetPercent);
}
public DesktopGridMetrics CalculateGridMetrics(
double hostWidth,
double hostHeight,
int shortSideCells,
double gapRatio,
double edgeInsetPx)
{
return _gridLayoutService.CalculateGridMetrics(
hostWidth,
hostHeight,
shortSideCells,
gapRatio,
edgeInsetPx);
}
} }
internal sealed class WallpaperSettingsService : IWallpaperSettingsService internal sealed class WallpaperSettingsService : IWallpaperSettingsService
@@ -234,7 +265,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
} }
} }
internal sealed class WeatherProviderAdapter : IWeatherProvider internal sealed class WeatherProviderAdapter : IWeatherProvider, IWeatherInfoService, IDisposable
{ {
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService(); private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
@@ -252,11 +283,20 @@ internal sealed class WeatherProviderAdapter : IWeatherProvider
{ {
return _weatherDataService.GetWeatherAsync(query, cancellationToken); return _weatherDataService.GetWeatherAsync(query, cancellationToken);
} }
public void Dispose()
{
if (_weatherDataService is IDisposable disposable)
{
disposable.Dispose();
}
}
} }
internal sealed class WeatherSettingsService : IWeatherSettingsService internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposable
{ {
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly WeatherProviderAdapter _weatherProvider = new();
public WeatherSettingsState Get() public WeatherSettingsState Get()
{ {
@@ -289,11 +329,22 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService
snapshot.WeatherLocationQuery = state.LocationQuery; snapshot.WeatherLocationQuery = state.LocationQuery;
_appSettingsService.Save(snapshot); _appSettingsService.Save(snapshot);
} }
public IWeatherInfoService GetWeatherInfoService()
{
return _weatherProvider;
}
public void Dispose()
{
_weatherProvider.Dispose();
}
} }
internal sealed class RegionSettingsService : IRegionSettingsService internal sealed class RegionSettingsService : IRegionSettingsService
{ {
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly TimeZoneService _timeZoneService = new();
public RegionSettingsState Get() public RegionSettingsState Get()
{ {
@@ -312,6 +363,11 @@ internal sealed class RegionSettingsService : IRegionSettingsService
: state.TimeZoneId.Trim(); : state.TimeZoneId.Trim();
_appSettingsService.Save(snapshot); _appSettingsService.Save(snapshot);
} }
public TimeZoneService GetTimeZoneService()
{
return _timeZoneService;
}
} }
internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable
@@ -388,13 +444,18 @@ internal sealed class LauncherPolicyService : ILauncherPolicyService
internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService
{ {
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly PluginRuntimeService? _pluginRuntimeService; private PluginRuntimeService? _pluginRuntimeService;
public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService) public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService)
{ {
_pluginRuntimeService = pluginRuntimeService; _pluginRuntimeService = pluginRuntimeService;
} }
public void SetPluginRuntime(PluginRuntimeService? pluginRuntimeService)
{
_pluginRuntimeService = pluginRuntimeService;
}
public PluginManagementSettingsState Get() public PluginManagementSettingsState Get()
{ {
var snapshot = _appSettingsService.Load(); var snapshot = _appSettingsService.Load();
@@ -426,9 +487,9 @@ internal sealed class PluginManagementSettingsService : IPluginManagementSetting
internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService, IDisposable internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService, IDisposable
{ {
private readonly PluginRuntimeService? _pluginRuntimeService; private PluginRuntimeService? _pluginRuntimeService;
private readonly AirAppMarketIndexService _indexService; private AirAppMarketIndexService _indexService;
private readonly AirAppMarketInstallService? _installService; private AirAppMarketInstallService? _installService;
private readonly Dictionary<string, AirAppMarketPluginEntry> _cachedPlugins = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, AirAppMarketPluginEntry> _cachedPlugins = new(StringComparer.OrdinalIgnoreCase);
public PluginMarketSettingsService(PluginRuntimeService? pluginRuntimeService) public PluginMarketSettingsService(PluginRuntimeService? pluginRuntimeService)
@@ -447,6 +508,24 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService
} }
} }
public void SetPluginRuntime(PluginRuntimeService? pluginRuntimeService)
{
_pluginRuntimeService = pluginRuntimeService;
_installService?.Dispose();
_installService = null;
if (_pluginRuntimeService is null)
{
return;
}
var dataRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop",
"PluginMarket");
_installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot);
}
public async Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default) public async Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default)
{ {
var result = await _indexService.LoadAsync(cancellationToken); var result = await _indexService.LoadAsync(cancellationToken);
@@ -567,6 +646,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
{ {
private readonly UpdateSettingsService _updateSettingsService; private readonly UpdateSettingsService _updateSettingsService;
private readonly PluginMarketSettingsService _pluginMarketSettingsService; private readonly PluginMarketSettingsService _pluginMarketSettingsService;
private readonly PluginManagementSettingsService _pluginManagementSettingsService;
private readonly WeatherSettingsService _weatherSettingsService;
public SettingsFacadeService(PluginRuntimeService? pluginRuntimeService = null) public SettingsFacadeService(PluginRuntimeService? pluginRuntimeService = null)
{ {
@@ -577,13 +658,15 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
WallpaperMedia = new WallpaperMediaService(); WallpaperMedia = new WallpaperMediaService();
Theme = new ThemeAppearanceService(); Theme = new ThemeAppearanceService();
StatusBar = new StatusBarSettingsService(); StatusBar = new StatusBarSettingsService();
Weather = new WeatherSettingsService(); _weatherSettingsService = new WeatherSettingsService();
Weather = _weatherSettingsService;
Region = new RegionSettingsService(); Region = new RegionSettingsService();
_updateSettingsService = new UpdateSettingsService(); _updateSettingsService = new UpdateSettingsService();
Update = _updateSettingsService; Update = _updateSettingsService;
LauncherCatalog = new LauncherCatalogService(); LauncherCatalog = new LauncherCatalogService();
LauncherPolicy = new LauncherPolicyService(); LauncherPolicy = new LauncherPolicyService();
PluginManagement = new PluginManagementSettingsService(pluginRuntimeService); _pluginManagementSettingsService = new PluginManagementSettingsService(pluginRuntimeService);
PluginManagement = _pluginManagementSettingsService;
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService); _pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
PluginMarket = _pluginMarketSettingsService; PluginMarket = _pluginMarketSettingsService;
ApplicationInfo = new ApplicationInfoService(); ApplicationInfo = new ApplicationInfoService();
@@ -619,8 +702,15 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
public IApplicationInfoService ApplicationInfo { get; } public IApplicationInfoService ApplicationInfo { get; }
public void BindPluginRuntime(PluginRuntimeService? pluginRuntimeService)
{
_pluginManagementSettingsService.SetPluginRuntime(pluginRuntimeService);
_pluginMarketSettingsService.SetPluginRuntime(pluginRuntimeService);
}
public void Dispose() public void Dispose()
{ {
_weatherSettingsService.Dispose();
_updateSettingsService.Dispose(); _updateSettingsService.Dispose();
_pluginMarketSettingsService.Dispose(); _pluginMarketSettingsService.Dispose();
} }

View File

@@ -19,7 +19,8 @@ internal sealed class SettingsService : ISettingsService
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly LauncherSettingsService _launcherSettingsService = new(); private readonly LauncherSettingsService _launcherSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new(); private readonly IComponentStateStore _componentStateStore = ComponentDomainStorageProvider.Instance;
private readonly IComponentMessageStore _componentMessageStore = ComponentDomainStorageProvider.Instance;
private readonly string _pluginSettingsPath; private readonly string _pluginSettingsPath;
private readonly object _pluginSettingsGate = new(); private readonly object _pluginSettingsGate = new();
@@ -80,7 +81,7 @@ internal sealed class SettingsService : ISettingsService
{ {
if (scope == SettingsScope.ComponentInstance) if (scope == SettingsScope.ComponentInstance)
{ {
return _componentSettingsService.LoadPluginSettings<T>(EnsureKey(subjectId), placementId); return _componentMessageStore.LoadSection<T>(EnsureKey(subjectId), placementId, EnsureKey(sectionId));
} }
if (scope != SettingsScope.Plugin) if (scope != SettingsScope.Plugin)
@@ -111,7 +112,7 @@ internal sealed class SettingsService : ISettingsService
{ {
if (scope == SettingsScope.ComponentInstance) if (scope == SettingsScope.ComponentInstance)
{ {
_componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, section); _componentMessageStore.SaveSection(EnsureKey(subjectId), placementId, EnsureKey(sectionId), section);
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys)); OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
return; return;
} }
@@ -142,7 +143,7 @@ internal sealed class SettingsService : ISettingsService
{ {
if (scope == SettingsScope.ComponentInstance) if (scope == SettingsScope.ComponentInstance)
{ {
_componentSettingsService.DeletePluginSettings(EnsureKey(subjectId), placementId); _componentMessageStore.DeleteSection(EnsureKey(subjectId), placementId, EnsureKey(sectionId));
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId)); OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId));
return; return;
} }
@@ -183,7 +184,11 @@ internal sealed class SettingsService : ISettingsService
SettingsScope.App => JsonSerializer.SerializeToElement(_appSettingsService.Load(), SerializerOptions), SettingsScope.App => JsonSerializer.SerializeToElement(_appSettingsService.Load(), SerializerOptions),
SettingsScope.Launcher => JsonSerializer.SerializeToElement(_launcherSettingsService.Load(), SerializerOptions), SettingsScope.Launcher => JsonSerializer.SerializeToElement(_launcherSettingsService.Load(), SerializerOptions),
SettingsScope.ComponentInstance => JsonSerializer.SerializeToElement( SettingsScope.ComponentInstance => JsonSerializer.SerializeToElement(
_componentSettingsService.LoadForComponent(EnsureKey(subjectId), placementId), LoadSection<Dictionary<string, JsonElement>>(
SettingsScope.ComponentInstance,
EnsureKey(subjectId),
sectionId ?? "__root__",
placementId),
SerializerOptions), SerializerOptions),
SettingsScope.Plugin => JsonSerializer.SerializeToElement( SettingsScope.Plugin => JsonSerializer.SerializeToElement(
LoadSection<Dictionary<string, JsonElement>>(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", placementId), LoadSection<Dictionary<string, JsonElement>>(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", placementId),
@@ -239,9 +244,10 @@ internal sealed class SettingsService : ISettingsService
if (scope == SettingsScope.ComponentInstance) if (scope == SettingsScope.ComponentInstance)
{ {
var dict = _componentSettingsService.LoadPluginSettings<Dictionary<string, JsonElement>>(EnsureKey(subjectId), placementId); var effectiveSection = sectionId ?? "__root__";
var dict = _componentMessageStore.LoadSection<Dictionary<string, JsonElement>>(EnsureKey(subjectId), placementId, effectiveSection);
dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone(); dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone();
_componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, dict); _componentMessageStore.SaveSection(EnsureKey(subjectId), placementId, effectiveSection, dict);
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys ?? [key])); OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys ?? [key]));
return; return;
} }
@@ -271,14 +277,14 @@ internal sealed class SettingsService : ISettingsService
private T LoadComponentSnapshot<T>(string? componentId, string? placementId) where T : new() private T LoadComponentSnapshot<T>(string? componentId, string? placementId) where T : new()
{ {
var snapshot = _componentSettingsService.LoadForComponent(EnsureKey(componentId), placementId); var snapshot = _componentStateStore.LoadState(EnsureKey(componentId), placementId);
return ConvertSnapshot<ComponentSettingsSnapshot, T>(snapshot); return ConvertSnapshot<ComponentSettingsSnapshot, T>(snapshot);
} }
private void SaveComponentSnapshot<T>(string? componentId, string? placementId, T snapshot) private void SaveComponentSnapshot<T>(string? componentId, string? placementId, T snapshot)
{ {
var converted = ConvertSnapshot<T, ComponentSettingsSnapshot>(snapshot); var converted = ConvertSnapshot<T, ComponentSettingsSnapshot>(snapshot);
_componentSettingsService.SaveForComponent(EnsureKey(componentId), placementId, converted); _componentStateStore.SaveState(EnsureKey(componentId), placementId, converted);
} }
private static TOut ConvertSnapshot<TIn, TOut>(TIn source) where TOut : new() private static TOut ConvertSnapshot<TIn, TOut>(TIn source) where TOut : new()

View File

@@ -9,10 +9,11 @@ using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware
{ {
private static readonly IReadOnlyDictionary<string, string> ZhCityNames = private static readonly IReadOnlyDictionary<string, string> ZhCityNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
@@ -58,8 +59,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
private string _componentId = BuiltInComponentIds.DesktopClock; private string _componentId = BuiltInComponentIds.DesktopClock;
private string _placementId = string.Empty; private string _placementId = string.Empty;
private readonly AppSettingsService _appSettingsService = new(); private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private TimeZoneService? _timeZoneService; private TimeZoneService? _timeZoneService;
private double _currentCellSize = 48; private double _currentCellSize = 48;
@@ -124,12 +124,6 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
RefreshFromSettings(); RefreshFromSettings();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
InitializeDialIfNeeded(); InitializeDialIfNeeded();
@@ -376,8 +370,11 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
private void LoadClockSettings() private void LoadClockSettings()
{ {
var appSnapshot = _appSettingsService.Load(); var appSnapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); var componentSnapshot = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
SettingsScope.ComponentInstance,
_componentId,
_placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var configuredTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId) var configuredTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId)

View File

@@ -10,10 +10,11 @@ using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware
{ {
private sealed record CourseItemViewModel( private sealed record CourseItemViewModel(
string Name, string Name,
@@ -26,8 +27,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromMinutes(4) Interval = TimeSpan.FromMinutes(4)
}; };
private readonly AppSettingsService _appSettingsService = new(); private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService(); private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
@@ -125,16 +125,13 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
RefreshSchedule(); RefreshSchedule();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshSchedule();
}
private void RefreshSchedule() private void RefreshSchedule()
{ {
var appSettings = _appSettingsService.Load(); var appSettings = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var componentSettings = _componentSettingsStore.LoadForComponent(_componentId, _placementId); var componentSettings = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
SettingsScope.ComponentInstance,
_componentId,
_placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode); _languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now; var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
UpdateHeader(now); UpdateHeader(now);

View File

@@ -16,10 +16,11 @@ using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, IRecommendationInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, IRecommendationInfoAwareComponentWidget, IComponentPlacementContextAware
{ {
private static readonly IReadOnlyDictionary<DayOfWeek, string> ZhWeekdays = private static readonly IReadOnlyDictionary<DayOfWeek, string> ZhWeekdays =
new Dictionary<DayOfWeek, string> new Dictionary<DayOfWeek, string>
@@ -58,8 +59,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromHours(6) Interval = TimeSpan.FromHours(6)
}; };
private readonly AppSettingsService _settingsService = new(); private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private IRecommendationInfoService _recommendationService = DefaultRecommendationService; private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
@@ -149,12 +149,6 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
RefreshFromSettings(); RefreshFromSettings();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
_isAttached = true; _isAttached = true;
@@ -651,7 +645,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
{ {
try try
{ {
var snapshot = _settingsService.Load(); var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode); _languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
} }
catch catch
@@ -664,7 +658,10 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
{ {
try try
{ {
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); var snapshot = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
SettingsScope.ComponentInstance,
_componentId,
_placementId);
return DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource); return DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
} }
catch catch

View File

@@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
@@ -14,7 +16,8 @@ public sealed record DesktopComponentControlFactoryContext(
IWeatherInfoService WeatherInfoService, IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService, IRecommendationInfoService RecommendationInfoService,
ICalculatorDataService CalculatorDataService, ICalculatorDataService CalculatorDataService,
IComponentInstanceSettingsStore ComponentSettingsStore, ISettingsService SettingsService,
IComponentSettingsAccessor ComponentSettingsAccessor,
string? PlacementId = null); string? PlacementId = null);
public sealed class DesktopComponentRuntimeRegistration public sealed class DesktopComponentRuntimeRegistration
@@ -84,9 +87,10 @@ public sealed class DesktopComponentRuntimeDescriptor
IWeatherInfoService weatherInfoService, IWeatherInfoService weatherInfoService,
IRecommendationInfoService recommendationInfoService, IRecommendationInfoService recommendationInfoService,
ICalculatorDataService calculatorDataService, ICalculatorDataService calculatorDataService,
IComponentInstanceSettingsStore componentSettingsStore,
string? placementId = null) string? placementId = null)
{ {
var settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId);
var control = _controlFactory(new DesktopComponentControlFactoryContext( var control = _controlFactory(new DesktopComponentControlFactoryContext(
Definition, Definition,
cellSize, cellSize,
@@ -94,12 +98,14 @@ public sealed class DesktopComponentRuntimeDescriptor
weatherInfoService, weatherInfoService,
recommendationInfoService, recommendationInfoService,
calculatorDataService, calculatorDataService,
componentSettingsStore, settingsService,
componentAccessor,
placementId)); placementId));
var runtimeContext = new DesktopComponentRuntimeContext( var runtimeContext = new DesktopComponentRuntimeContext(
Definition.Id, Definition.Id,
placementId, placementId,
componentSettingsStore); settingsService,
componentAccessor);
if (control is IComponentRuntimeContextAware runtimeContextAwareComponent) if (control is IComponentRuntimeContextAware runtimeContextAwareComponent)
{ {
@@ -111,13 +117,6 @@ public sealed class DesktopComponentRuntimeDescriptor
placementAwareComponent.SetComponentPlacementContext(Definition.Id, placementId); placementAwareComponent.SetComponentPlacementContext(Definition.Id, placementId);
} }
if (control is IComponentSettingsStoreAware settingsStoreAwareComponent)
{
settingsStoreAwareComponent.SetComponentSettingsStore(componentSettingsStore);
}
ComponentSettingsService.ApplyScopedContextToTarget(control, Definition.Id, placementId);
if (control is IDesktopComponentWidget sizedComponent) if (control is IDesktopComponentWidget sizedComponent)
{ {
sizedComponent.ApplyCellSize(cellSize); sizedComponent.ApplyCellSize(cellSize);

View File

@@ -12,11 +12,12 @@ using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Theme; using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware
{ {
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes; private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
@@ -25,8 +26,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval }; private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval };
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1); private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
private readonly TranslateTransform _backgroundMotionTranslateTransform = new(); private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
private readonly AppSettingsService _settingsService = new(); private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService; private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;
@@ -185,12 +185,6 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
RefreshFromSettings(); RefreshFromSettings();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{ {
_ = isEditMode; _ = isEditMode;
@@ -265,7 +259,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
} }
_isRefreshing = true; _isRefreshing = true;
var app = _settingsService.Load(); var app = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
_languageCode = _localizationService.NormalizeLanguageCode(app.LanguageCode); _languageCode = _localizationService.NormalizeLanguageCode(app.LanguageCode);
var locale = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) ? "zh_cn" : "en_us"; var locale = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) ? "zh_cn" : "en_us";
var latitude = double.IsFinite(app.WeatherLatitude) ? Math.Clamp(app.WeatherLatitude, -90, 90) : 39.9042; var latitude = double.IsFinite(app.WeatherLatitude) ? Math.Clamp(app.WeatherLatitude, -90, 90) : 39.9042;
@@ -953,7 +947,10 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
try try
{ {
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); var snapshot = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
SettingsScope.ComponentInstance,
_componentId,
_placementId);
enabled = snapshot.WeatherAutoRefreshEnabled; enabled = snapshot.WeatherAutoRefreshEnabled;
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes); intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
} }

View File

@@ -18,7 +18,7 @@ using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware
{ {
private enum WeatherVisualKind private enum WeatherVisualKind
{ {
@@ -236,12 +236,6 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
RefreshFromSettings(); RefreshFromSettings();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{ {
_ = isEditMode; _ = isEditMode;

View File

@@ -16,7 +16,7 @@ using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware
{ {
private enum WeatherVisualKind private enum WeatherVisualKind
{ {
@@ -234,12 +234,6 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
RefreshFromSettings(); RefreshFromSettings();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{ {
_ = isEditMode; _ = isEditMode;

View File

@@ -16,7 +16,7 @@ using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware
{ {
private sealed record WeatherClockConfig( private sealed record WeatherClockConfig(
string LanguageCode, string LanguageCode,
@@ -128,12 +128,6 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
RefreshFromSettings(); RefreshFromSettings();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void ApplyCellSize(double cellSize) public void ApplyCellSize(double cellSize)
{ {
_currentCellSize = Math.Max(1, cellSize); _currentCellSize = Math.Max(1, cellSize);

View File

@@ -18,7 +18,7 @@ using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware
{ {
private enum WeatherVisualKind private enum WeatherVisualKind
{ {
@@ -179,12 +179,6 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
RefreshFromSettings(); RefreshFromSettings();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{ {
_ = isEditMode; _ = isEditMode;

View File

@@ -13,7 +13,7 @@ using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware
{ {
private const int BaseWidthCells = 4; private const int BaseWidthCells = 4;
private const int BaseHeightCells = 2; private const int BaseHeightCells = 2;
@@ -159,12 +159,6 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
RefreshFromSettings(); RefreshFromSettings();
} }
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void ApplyCellSize(double cellSize) public void ApplyCellSize(double cellSize)
{ {
_currentCellSize = Math.Max(1, cellSize); _currentCellSize = Math.Max(1, cellSize);

View File

@@ -101,19 +101,12 @@ public partial class MainWindow
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e) private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
{ {
// "Desktop edit" toggle. While editing, show the component library window. _componentLibraryWindowService.Toggle(this);
if (_isComponentLibraryOpen)
{
CloseComponentLibraryWindow(reopenSettings: false);
return;
}
OpenComponentLibraryWindow();
} }
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e) private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
{ {
CloseComponentLibraryWindow(reopenSettings: false); _componentLibraryWindowService.Close(this);
} }
private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e) private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e)
@@ -202,21 +195,6 @@ public partial class MainWindow
{ {
ClockWidget.SetDisplayFormat(_clockDisplayFormat); ClockWidget.SetDisplayFormat(_clockDisplayFormat);
} }
if (_clockDisplayFormat == ClockDisplayFormat.HourMinute)
{
if (ClockFormatHMRadio is not null)
{
ClockFormatHMRadio.IsChecked = true;
}
}
else
{
if (ClockFormatHMSSRadio is not null)
{
ClockFormatHMSSRadio.IsChecked = true;
}
}
} }
private void ApplyTopStatusComponentVisibility() private void ApplyTopStatusComponentVisibility()
@@ -233,15 +211,6 @@ public partial class MainWindow
Grid.SetColumnSpan(ClockWidget, columnSpan); Grid.SetColumnSpan(ClockWidget, columnSpan);
} }
} }
if (WallpaperPreviewClockWidget is not null)
{
WallpaperPreviewClockWidget.IsVisible = showClock;
if (showClock)
{
WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
}
}
} }
private TaskbarContext GetCurrentTaskbarContext() private TaskbarContext GetCurrentTaskbarContext()
@@ -251,11 +220,7 @@ public partial class MainWindow
private void ApplyTaskbarActionVisibility(TaskbarContext context) private void ApplyTaskbarActionVisibility(TaskbarContext context)
{ {
if (BackToWindowsButton is null || if (BackToWindowsButton is null || OpenComponentLibraryButton is null)
OpenComponentLibraryButton is null ||
WallpaperPreviewBackButtonVisual is null ||
WallpaperPreviewComponentLibraryVisual is null ||
WallpaperPreviewSettingsButtonIcon is null)
{ {
return; return;
} }
@@ -266,9 +231,6 @@ public partial class MainWindow
BackToWindowsButton.IsVisible = showMinimize; BackToWindowsButton.IsVisible = showMinimize;
OpenComponentLibraryButton.IsVisible = showDesktopEdit; OpenComponentLibraryButton.IsVisible = showDesktopEdit;
WallpaperPreviewBackButtonVisual.IsVisible = showMinimize;
WallpaperPreviewComponentLibraryVisual.IsVisible = showDesktopEdit;
WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings;
if (TaskbarFixedActionsHost is not null) if (TaskbarFixedActionsHost is not null)
{ {
@@ -280,16 +242,6 @@ public partial class MainWindow
TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit; TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit;
} }
if (WallpaperPreviewTaskbarFixedActionsHost is not null)
{
WallpaperPreviewTaskbarFixedActionsHost.IsVisible = showMinimize;
}
if (WallpaperPreviewTaskbarSettingsActionHost is not null)
{
WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit;
}
var dynamicActions = ResolveDynamicTaskbarActions(context) var dynamicActions = ResolveDynamicTaskbarActions(context)
.Where(action => action.IsVisible) .Where(action => action.IsVisible)
.ToList(); .ToList();
@@ -300,11 +252,6 @@ public partial class MainWindow
{ {
TaskbarDynamicActionsHost.IsVisible = hasDynamicActions; TaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
} }
if (WallpaperPreviewTaskbarDynamicActionsHost is not null)
{
WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
}
} }
private void UpdateOpenSettingsActionVisualState() private void UpdateOpenSettingsActionVisualState()
@@ -455,14 +402,7 @@ public partial class MainWindow
TaskbarDynamicActionsPanel.Children.Clear(); TaskbarDynamicActionsPanel.Children.Clear();
} }
if (WallpaperPreviewTaskbarDynamicActionsHost is not null) if (actions.Count == 0 || TaskbarDynamicActionsPanel is null)
{
WallpaperPreviewTaskbarDynamicActionsHost.Children.Clear();
}
if (actions.Count == 0 ||
TaskbarDynamicActionsPanel is null ||
WallpaperPreviewTaskbarDynamicActionsHost is null)
{ {
return; return;
} }
@@ -556,37 +496,6 @@ public partial class MainWindow
TaskbarDynamicActionsPanel.Children.Add(button); TaskbarDynamicActionsPanel.Children.Add(button);
Control previewIcon = new SymbolIcon
{
Symbol = iconSymbol,
IconVariant = IconVariant.Regular,
FontSize = iconSize * 0.85
};
var previewText = new TextBlock
{
Text = action.Title,
FontSize = fontSize * 0.85,
Foreground = (isDeleteAction || isHideAction)
? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
: Foreground,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
var previewContent = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = spacing * 0.5,
Children = { previewIcon, previewText }
};
var previewBorder = new Border
{
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Child = previewContent
};
WallpaperPreviewTaskbarDynamicActionsHost.Children.Add(previewBorder);
} }
} }
@@ -637,7 +546,7 @@ public partial class MainWindow
} }
_desktopComponentPlacements.Remove(placement); _desktopComponentPlacements.Remove(placement);
_componentSettingsService.DeleteForComponent(placement.ComponentId, placement.PlacementId); _componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId);
ClearDesktopComponentSelection(); ClearDesktopComponentSelection();
@@ -698,7 +607,7 @@ public partial class MainWindow
foreach (var placement in placementsToRemove) foreach (var placement in placementsToRemove)
{ {
_desktopComponentPlacements.Remove(placement); _desktopComponentPlacements.Remove(placement);
_componentSettingsService.DeleteForComponent(placement.ComponentId, placement.PlacementId); _componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId);
} }
_desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount); _desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount);
@@ -1345,14 +1254,19 @@ public partial class MainWindow
{ {
try try
{ {
var component = runtimeDescriptor.CreateControl( var createContext = new ComponentLibraryCreateContext(
cellSize, cellSize,
_timeZoneService, _timeZoneService,
_weatherDataService, _weatherDataService,
_recommendationInfoService, _recommendationInfoService,
_calculatorDataService, _calculatorDataService,
_componentSettingsService,
placementId); placementId);
if (!_componentLibraryService.TryCreateControl(runtimeDescriptor.Definition.Id, createContext, out var component, out var exception) ||
component is null)
{
throw exception ?? new InvalidOperationException("Component library service returned no control.");
}
component.Classes.Add(DesktopComponentClass); component.Classes.Add(DesktopComponentClass);
return component; return component;
} }
@@ -1376,6 +1290,18 @@ public partial class MainWindow
} }
} }
internal bool IsComponentLibraryOpenFromService => _isComponentLibraryOpen;
internal void OpenComponentLibraryWindowFromService()
{
OpenComponentLibraryWindow();
}
internal void CloseComponentLibraryWindowFromService()
{
CloseComponentLibraryWindow(reopenSettings: false);
}
private void CollapseComponentLibraryPanel() private void CollapseComponentLibraryPanel()
{ {
// Animate component library panel collapsing downward // Animate component library panel collapsing downward

View File

@@ -1,456 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using FluentIcons.Avalonia;
using FluentIcons.Common;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private const string AppCodeName = "Administrate";
private const string AppFontName = "MiSans";
private const string FallbackAppVersion = "1.0.0";
private static readonly IReadOnlyDictionary<string, string> ZhTimeZoneNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["China Standard Time"] = "中国标准时间",
["Asia/Shanghai"] = "中国标准时间",
["Tokyo Standard Time"] = "日本标准时间",
["Asia/Tokyo"] = "日本标准时间",
["Pacific Standard Time"] = "太平洋标准时间",
["America/Los_Angeles"] = "太平洋标准时间",
["Eastern Standard Time"] = "美国东部标准时间",
["America/New_York"] = "美国东部标准时间",
["Central European Standard Time"] = "中欧标准时间",
["Europe/Berlin"] = "中欧标准时间",
["GMT Standard Time"] = "格林威治标准时间",
["Europe/London"] = "格林威治标准时间",
["UTC"] = "协调世界时",
["Etc/UTC"] = "协调世界时"
};
private void InitializeLocalization(string? languageCode)
{
_languageCode = _localizationService.NormalizeLanguageCode(languageCode);
if (LanguageComboBox is null)
{
return;
}
_suppressLanguageSelectionEvents = true;
LanguageComboBox.SelectedIndex = string.Equals(_languageCode, "en-US", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
_suppressLanguageSelectionEvents = false;
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
private string Lf(string key, string fallback, params object[] args)
{
var template = L(key, fallback);
return string.Format(template, args);
}
private string GetLanguageDisplayName(string languageCode)
{
return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase)
? L("settings.region.language_en", "English")
: L("settings.region.language_zh", "Chinese");
}
private string GetLocalizedPlacementDisplayName(WallpaperPlacement placement)
{
return placement switch
{
WallpaperPlacement.Fill => L("placement.fill", "Fill"),
WallpaperPlacement.Fit => L("placement.fit", "Fit"),
WallpaperPlacement.Stretch => L("placement.stretch", "Stretch"),
WallpaperPlacement.Center => L("placement.center", "Center"),
WallpaperPlacement.Tile => L("placement.tile", "Tile"),
_ => L("placement.fill", "Fill")
};
}
private void ApplyLocalization()
{
Title = L("app.title", "LanMountainDesktop");
BackToWindowsTextBlock.Text = L("button.back_to_windows", "Back to Windows");
WallpaperPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows");
ToolTip.SetTip(BackToWindowsButton, L("tooltip.back_to_windows", "Back to Windows"));
OpenComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
GridPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "Edit Desktop"));
ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Widgets");
ToolTip.SetTip(CloseComponentLibraryButton, L("common.close", "Close"));
ComponentLibraryEmptyTextBlock.Text = L(
"component_library.empty",
"Swipe to pick a category, tap to open, then drag a widget onto the desktop.");
LauncherTitleTextBlock.Text = L("launcher.title", "App Launcher");
LauncherSubtitleTextBlock.Text = OperatingSystem.IsLinux()
? L(
"launcher.subtitle_linux",
"Displays installed apps discovered from Linux desktop entries.")
: L(
"launcher.subtitle",
"Displays all apps and folders based on the Windows Start menu structure.");
ToolTip.SetTip(LauncherFolderBackButton, L("common.back", "Back"));
ToolTip.SetTip(LauncherFolderCloseButton, L("common.close", "Close"));
// SettingsNavHeaderTextBlock no longer exists
SettingsNavWallpaperItem.Content = L("settings.nav.wallpaper", "Wallpaper");
SettingsNavGridItem.Content = L("settings.nav.grid", "Grid");
SettingsNavColorItem.Content = L("settings.nav.color", "Color");
SettingsNavStatusBarItem.Content = L("settings.nav.status_bar", "Status Bar");
SettingsNavWeatherItem.Content = L("settings.nav.weather", "Weather");
SettingsNavRegionItem.Content = L("settings.nav.region", "Region");
SettingsNavUpdateItem.Content = L("settings.nav.update", "Update");
SettingsNavLauncherItem.Content = L("settings.nav.launcher", "App Launcher");
SettingsNavPluginsItem.Content = L("settings.nav.plugins", "Plugins");
SettingsNavPluginMarketItem.Content = L("settings.nav.plugin_market", "Plugin Market");
WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "Personalize your wallpaper");
WallpaperPlacementSettingsExpander.Header = L("settings.wallpaper.placement_label", "Placement");
WallpaperPlacementSettingsExpander.Description = L(
"settings.wallpaper.placement_desc",
"Adjust how the image fits on the desktop.");
PickWallpaperButton.Content = L("settings.wallpaper.pick_button", "Browse");
ClearWallpaperButton.Content = L("settings.wallpaper.clear_button", "Reset");
GridPanelTitleTextBlock.Text = L("settings.grid.title", "Grid Layout");
GridSpacingSettingsExpander.Header = L("settings.grid.spacing_label", "Grid Spacing");
GridSpacingRelaxedComboBoxItem.Content = L("settings.grid.spacing_relaxed", "Relaxed");
GridSpacingCompactComboBoxItem.Content = L("settings.grid.spacing_compact", "Compact");
GridEdgeInsetSettingsExpander.Header = L("settings.grid.edge_inset_label", "Screen Inset");
ApplyGridButton.Content = L("settings.grid.apply_button", "Apply");
UpdateGridEdgeInsetComputedPxText(_currentDesktopCellSize);
ColorPanelTitleTextBlock.Text = L("settings.color.title", "Color");
ThemeModeSettingsExpander.Header = L("settings.color.day_night_label", "Day/Night");
NightModeToggleSwitch.OffContent = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 6,
Children =
{
new SymbolIcon { Symbol = Symbol.WeatherSunny, IconVariant = IconVariant.Regular, FontSize = 14 },
new TextBlock
{
Text = L("settings.color.day_night_off", "Day"),
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
}
}
};
NightModeToggleSwitch.OnContent = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 6,
Children =
{
new SymbolIcon { Symbol = Symbol.WeatherMoon, IconVariant = IconVariant.Regular, FontSize = 14 },
new TextBlock
{
Text = L("settings.color.day_night_on", "Night"),
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
}
}
};
RecommendedColorsLabelTextBlock.Text = L("settings.color.recommended_label", "Recommended Colors");
SystemMonetColorsLabelTextBlock.Text = L("settings.color.system_monet_label", "System Monet Colors");
RefreshMonetColorsButton.Content = L("settings.color.refresh_button", "Refresh");
StatusBarPanelTitleTextBlock.Text = L("settings.status_bar.title", "Status Bar");
StatusBarClockSettingsExpander.Header = L("settings.status_bar.clock_header", "Clock");
StatusBarSpacingSettingsExpander.Header = L("settings.status_bar.spacing_header", "Component Spacing");
StatusBarSpacingSettingsExpander.Description = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
StatusBarSpacingModeCompactItem.Content = L("settings.status_bar.spacing_mode_compact", "Compact");
StatusBarSpacingModeRelaxedItem.Content = L("settings.status_bar.spacing_mode_relaxed", "Relaxed");
StatusBarSpacingModeCustomItem.Content = L("settings.status_bar.spacing_mode_custom", "Custom");
StatusBarSpacingCustomPanel.Content = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
WeatherPanelTitleTextBlock.Text = L("settings.weather.title", "Weather");
WeatherPreviewSectionTextBlock.Text = L("settings.weather.preview_section", "Weather Preview");
WeatherSettingsSectionTextBlock.Text = L("settings.weather.settings_section", "Settings");
WeatherPreviewSettingsExpander.Header = L("settings.weather.preview_panel_header", "Weather Preview");
WeatherPreviewSettingsExpander.Description = L(
"settings.weather.preview_panel_desc",
"Refresh and verify current weather service status.");
WeatherPreviewButton.Content = L("settings.weather.refresh_button", "Refresh");
WeatherLocationSettingsExpander.Header = L("settings.weather.location_source_header", "Location Source");
WeatherLocationSettingsExpander.Description = L(
"settings.weather.location_source_desc",
"Choose how weather widgets resolve location.");
WeatherLocationModeCityItem.Content = L("settings.weather.mode_city_search", "City Search");
WeatherLocationModeCoordinatesItem.Content = L("settings.weather.mode_coordinates", "Coordinates");
WeatherLocationModeCityChipItem.Content = L("settings.weather.mode_city_search", "City Search");
WeatherLocationModeCoordinatesChipItem.Content = L("settings.weather.mode_coordinates", "Coordinates");
WeatherAutoRefreshToggleSwitch.Content = L("settings.weather.auto_refresh", "Auto refresh location on startup");
WeatherLocationSelectionTitleTextBlock.Text = L("settings.weather.city_selection_label", "City Selection");
WeatherLocationSelectionDescriptionTextBlock.Text = L(
"settings.weather.location_city_summary_desc",
"Select the current city used for weather queries.");
WeatherCitySearchSettingsExpander.Header = L("settings.weather.city_search_header", "City Search");
WeatherCitySearchSettingsExpander.Description = L(
"settings.weather.city_search_desc",
"Search cities and apply one weather location.");
WeatherCitySearchTextBox.Watermark = L("settings.weather.search_placeholder", "e.g. Beijing");
WeatherSearchButton.Content = L("settings.weather.search_button", "Search");
WeatherApplyCityButton.Content = L("settings.weather.apply_city_button", "Apply City");
WeatherCoordinateSettingsExpander.Header = L("settings.weather.coordinates_header", "Coordinates");
WeatherCoordinateSettingsExpander.Description = L(
"settings.weather.coordinates_desc",
"Set latitude/longitude and optional key/name.");
WeatherLatitudeNumberBox.Header = L("settings.weather.latitude_label", "Latitude");
WeatherLongitudeNumberBox.Header = L("settings.weather.longitude_label", "Longitude");
WeatherLocationKeyTextBox.Watermark = L("settings.weather.location_key_placeholder", "Location key (optional)");
WeatherLocationNameTextBox.Watermark = L("settings.weather.location_name_placeholder", "Display name (optional)");
WeatherApplyCoordinatesButton.Content = L("settings.weather.apply_coordinates_button", "Apply Coordinates");
WeatherAlertFilterSettingsExpander.Header = L("settings.weather.alert_filter_header", "Excluded Alerts");
WeatherAlertFilterSettingsExpander.Description = L(
"settings.weather.alert_filter_desc",
"Alerts containing these words will not be shown. One rule per line.");
WeatherAlertListTitleTextBlock.Text = L("settings.weather.alert_list_label", "Exclude List");
WeatherAlertListDescriptionTextBlock.Text = L("settings.weather.alert_list_desc", "One exclusion rule per line.");
WeatherExcludedAlertsTextBox.Watermark = L("settings.weather.alert_filter_placeholder", "One keyword per line");
WeatherIconPackSettingsExpander.Header = L("settings.weather.icon_style_header", "Weather Icon Style");
WeatherIconPackSettingsExpander.Description = L(
"settings.weather.icon_style_desc",
"Choose Fluent Icon style for weather symbols.");
WeatherIconPackFluentRegularItem.Content = L("settings.weather.icon_style_fluent_regular", "Fluent Regular");
WeatherIconPackFluentFilledItem.Content = L("settings.weather.icon_style_fluent_filled", "Fluent Filled");
WeatherNoTlsSettingsExpander.Header = L("settings.weather.no_tls_header", "No TLS Weather Request");
WeatherNoTlsSettingsExpander.Description = L(
"settings.weather.no_tls_desc",
"Not recommended. Enable only for incompatible network environments.");
WeatherNoTlsToggleSwitch.Content = L("settings.weather.no_tls_toggle", "Allow non-TLS request fallback");
WeatherFooterHintTextBlock.Text = L(
"settings.weather.footer_hint",
"Desktop weather widgets will reuse the location and alert exclusion settings configured here.");
if (string.IsNullOrWhiteSpace(_weatherSearchKeyword))
{
WeatherSearchStatusTextBlock.Text = L(
"settings.weather.search_hint",
"Search by city name and apply one location.");
}
if (!_isWeatherPreviewInProgress)
{
WeatherPreviewResultTextBlock.Text = L(
"settings.weather.preview_hint",
"Use test fetch to verify your weather configuration.");
}
UpdateWeatherLocationStatusText();
RegionPanelTitleTextBlock.Text = L("settings.region.title", "Region");
LanguageSettingsExpander.Header = L("settings.region.language_header", "Language");
LanguageSettingsExpander.Description = L("settings.region.language_desc", "Select application language. Changes apply immediately.");
LanguageChineseItem.Content = L("settings.region.language_zh", "Chinese");
LanguageEnglishItem.Content = L("settings.region.language_en", "English");
TimeZoneSettingsExpander.Header = L("settings.region.timezone_header", "Time Zone");
TimeZoneSettingsExpander.Description = L(
"settings.region.timezone_desc",
"Select a time zone. Clock and calendar widgets will follow this zone.");
ApplyUpdateLocalization();
LauncherSettingsPanelTitleTextBlock.Text = L("settings.launcher.title", "App Launcher");
LauncherHiddenItemsSettingsExpander.Header = L("settings.launcher.hidden_header", "Hidden Items");
LauncherHiddenItemsSettingsExpander.Description = L(
"settings.launcher.hidden_desc",
"Review hidden launcher entries and show them again.");
LauncherHiddenItemsDescriptionTextBlock.Text = L(
"settings.launcher.hidden_hint",
"Right-click an icon in launcher to hide it. Hidden entries appear here.");
LauncherHiddenItemsEmptyTextBlock.Text = L("settings.launcher.hidden_empty", "No hidden items.");
ApplyPluginSettingsLocalization();
ApplyPluginMarketSettingsLocalization();
SettingsNavAboutItem.Content = L("settings.nav.about", "About");
AboutPanelTitleTextBlock.Text = L("settings.about.title", "About");
VersionTextBlock.Text = Lf(
"settings.about.version_format",
"Version: {0}",
GetAppVersionText());
CodeNameTextBlock.Text = Lf(
"settings.about.codename_format",
"Code Name: {0}",
AppCodeName);
FontInfoTextBlock.Text = Lf(
"settings.about.font_format",
"Font: {0}",
AppFontName);
AboutStartupSettingsExpander.Header = L("settings.about.startup_header", "Windows Startup");
AboutStartupSettingsExpander.Description = L(
"settings.about.startup_desc",
"Launch the app automatically when signing in to Windows.");
AboutRenderModeSettingsExpander.Header = L("settings.about.render_mode_header", "Rendering Mode");
AboutRenderModeSettingsExpander.Description = L(
"settings.about.render_mode_desc",
"Choose the rendering backend. Restart the app after changing this option. Unsupported modes fall back to software.");
SetAppRenderModeComboItemContent(AppRenderingModeHelper.Default, L("settings.about.render_mode.default", "Default"));
SetAppRenderModeComboItemContent(AppRenderingModeHelper.Software, L("settings.about.render_mode.software", "Software"));
SetAppRenderModeComboItemContent(AppRenderingModeHelper.AngleEgl, L("settings.about.render_mode.angle_egl", "angleEgl"));
SetAppRenderModeComboItemContent(AppRenderingModeHelper.Wgl, L("settings.about.render_mode.wgl", "WGL"));
SetAppRenderModeComboItemContent(AppRenderingModeHelper.Vulkan, L("settings.about.render_mode.vulkan", "Vulkan"));
UpdateCurrentRenderBackendStatus();
UpdatePendingRestartDock();
if (WallpaperPlacementComboBox?.ItemCount >= 5)
{
if (WallpaperPlacementComboBox.Items[0] is ComboBoxItem fillItem) fillItem.Content = L("placement.fill", "Fill");
if (WallpaperPlacementComboBox.Items[1] is ComboBoxItem fitItem) fitItem.Content = L("placement.fit", "Fit");
if (WallpaperPlacementComboBox.Items[2] is ComboBoxItem stretchItem) stretchItem.Content = L("placement.stretch", "Stretch");
if (WallpaperPlacementComboBox.Items[3] is ComboBoxItem centerItem) centerItem.Content = L("placement.center", "Center");
if (WallpaperPlacementComboBox.Items[4] is ComboBoxItem tileItem) tileItem.Content = L("placement.tile", "Tile");
}
GridInfoTextBlock.Text = Lf(
"settings.grid.info_format",
"Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
DesktopGrid.ColumnDefinitions.Count,
DesktopGrid.RowDefinitions.Count,
DesktopGrid.RowDefinitions.Count > 0 ? DesktopGrid.RowDefinitions[0].Height.Value : 0d);
InitializeTimeZoneSettings();
BuildComponentLibraryCategoryPages();
RenderLauncherRootTiles();
RenderLauncherHiddenItemsList();
UpdateOpenSettingsActionVisualState();
UpdateWallpaperDisplay();
}
private void SetAppRenderModeComboItemContent(string tag, string content)
{
var item = AppRenderModeComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(candidate =>
string.Equals(candidate.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase));
if (item is not null)
{
item.Content = content;
}
}
private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone)
{
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
var sign = offset >= TimeSpan.Zero ? "+" : "-";
var hours = Math.Abs(offset.Hours);
var minutes = Math.Abs(offset.Minutes);
var name = string.IsNullOrWhiteSpace(timeZone.StandardName)
? timeZone.DisplayName
: timeZone.StandardName;
if (string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) &&
ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName))
{
name = localizedName;
}
if (string.IsNullOrWhiteSpace(name))
{
name = timeZone.Id;
}
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {name}";
}
private static string GetAppVersionText()
{
var version = typeof(MainWindow).Assembly.GetName().Version;
if (version is null || version.Major < 0 || version.Minor < 0 || version.Build < 0)
{
return FallbackAppVersion;
}
return $"{version.Major}.{version.Minor}.{version.Build}";
}
private void OnLanguageSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressLanguageSelectionEvents || LanguageComboBox?.SelectedItem is not ComboBoxItem item)
{
return;
}
var selectedLanguage = item.Tag as string;
_languageCode = _localizationService.NormalizeLanguageCode(selectedLanguage);
ApplyLocalization();
ThemeColorStatusTextBlock.Text = Lf(
"settings.region.applied_format",
"Language switched to: {0}",
GetLanguageDisplayName(_languageCode));
PersistSettings();
}
private void UpdateWeatherLocationStatusText()
{
if (WeatherLocationStatusTextBlock is null)
{
return;
}
var modeText = _weatherLocationMode == WeatherLocationMode.Coordinates
? L("settings.weather.mode_coordinates", "Coordinates")
: L("settings.weather.mode_city_search", "City Search");
if (_weatherLocationMode == WeatherLocationMode.CitySearch)
{
if (string.IsNullOrWhiteSpace(_weatherLocationKey))
{
WeatherLocationStatusTextBlock.Text = L(
"settings.weather.status_city_empty",
"No city location is configured.");
UpdateWeatherLocationSummaryCard();
return;
}
var locationName = string.IsNullOrWhiteSpace(_weatherLocationName)
? _weatherLocationKey
: _weatherLocationName;
WeatherLocationStatusTextBlock.Text = Lf(
"settings.weather.status_city_format",
"Mode: {0} | {1} | Key: {2}",
modeText,
locationName,
_weatherLocationKey);
UpdateWeatherLocationSummaryCard();
return;
}
WeatherLocationStatusTextBlock.Text = Lf(
"settings.weather.status_coordinates_format",
"Mode: {0} | Lat {1:F4}, Lon {2:F4} | Key: {3}",
modeText,
_weatherLatitude,
_weatherLongitude,
string.IsNullOrWhiteSpace(_weatherLocationKey)
? BuildCoordinateLocationKey(_weatherLatitude, _weatherLongitude)
: _weatherLocationKey);
UpdateWeatherLocationSummaryCard();
}
}

View File

@@ -7,6 +7,13 @@ public partial class MainWindow
{ {
private void UpdateCurrentRenderBackendStatus() private void UpdateCurrentRenderBackendStatus()
{ {
if (CurrentRenderBackendLabelTextBlock is null ||
CurrentRenderBackendValueTextBlock is null ||
CurrentRenderBackendImplementationTextBlock is null)
{
return;
}
var backendInfo = AppRenderBackendDiagnostics.Detect(); var backendInfo = AppRenderBackendDiagnostics.Detect();
var localizedBackend = GetLocalizedRenderBackendName(backendInfo.ActualBackend); var localizedBackend = GetLocalizedRenderBackendName(backendInfo.ActualBackend);

View File

@@ -1,115 +0,0 @@
using System.Threading.Tasks;
using Avalonia.Interactivity;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private bool _isRestartPromptVisible;
private void OnPendingRestartStateChanged()
{
if (Dispatcher.UIThread.CheckAccess())
{
UpdatePendingRestartDock();
return;
}
Dispatcher.UIThread.Post(UpdatePendingRestartDock);
}
private void UpdatePendingRestartDock()
{
PendingRestartDock.IsVisible = PendingRestartStateService.HasPendingRestart;
PendingRestartDockTitleTextBlock.Text = L("settings.restart_dock.title", "Restart required");
PendingRestartDockDescriptionTextBlock.Text = L(
"settings.restart_dock.description",
"Some changes will take effect after restarting the app.");
PendingRestartDockButtonTextBlock.Text = L("settings.restart_dock.button", "Restart app");
}
private async void OnPendingRestartDockButtonClick(object? sender, RoutedEventArgs e)
{
await ShowGenericRestartPromptAsync();
}
private Task ShowRenderModeRestartPromptAsync(string selectedMode)
{
var message = Lf(
"settings.restart_dialog.render_mode_message",
"Restart the app to switch the rendering mode from \"{0}\" to \"{1}\". Restart now?",
GetLocalizedAppRenderModeDisplayName(_runningAppRenderMode),
GetLocalizedAppRenderModeDisplayName(selectedMode));
return ShowRestartPromptCoreAsync(message);
}
private Task ShowGenericRestartPromptAsync()
{
return ShowRestartPromptCoreAsync(L(
"settings.restart_dock.description",
"Some changes will take effect after restarting the app."));
}
private async Task ShowRestartPromptCoreAsync(string message)
{
if (_isRestartPromptVisible)
{
return;
}
_isRestartPromptVisible = true;
try
{
var dialog = new ContentDialog
{
Title = L("settings.restart_dialog.title", "Restart required"),
Content = message,
PrimaryButtonText = L("settings.restart_dialog.restart", "Restart now"),
CloseButtonText = L("settings.restart_dialog.cancel", "Cancel"),
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync(this);
if (result == ContentDialogResult.Primary)
{
if (App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
Source: nameof(MainWindow),
Reason: "User confirmed a pending restart prompt.")) != true)
{
UpdatePendingRestartDock();
}
return;
}
UpdatePendingRestartDock();
}
finally
{
_isRestartPromptVisible = false;
}
}
private string GetLocalizedAppRenderModeDisplayName(string renderMode)
{
if (renderMode == AppRenderBackendDiagnostics.Unknown)
{
return L("settings.about.render_mode.unknown", "Unknown");
}
return AppRenderingModeHelper.Normalize(renderMode) switch
{
AppRenderingModeHelper.Software => L("settings.about.render_mode.software", "Software"),
AppRenderingModeHelper.AngleEgl => L("settings.about.render_mode.angle_egl", "angleEgl"),
AppRenderingModeHelper.Wgl => L("settings.about.render_mode.wgl", "WGL"),
AppRenderingModeHelper.Vulkan => L("settings.about.render_mode.vulkan", "Vulkan"),
_ => L("settings.about.render_mode.default", "Default")
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,404 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private TextBlock? CurrentRenderBackendLabelTextBlock => this.FindControl<TextBlock>("CurrentRenderBackendLabelTextBlock");
private TextBlock? CurrentRenderBackendValueTextBlock => this.FindControl<TextBlock>("CurrentRenderBackendValueTextBlock");
private TextBlock? CurrentRenderBackendImplementationTextBlock => this.FindControl<TextBlock>("CurrentRenderBackendImplementationTextBlock");
private Slider? GridSizeSlider => this.FindControl<Slider>("GridSizeSlider");
private NumberBox? GridSizeNumberBox => this.FindControl<NumberBox>("GridSizeNumberBox");
private Slider? GridEdgeInsetSlider => this.FindControl<Slider>("GridEdgeInsetSlider");
private NumberBox? GridEdgeInsetNumberBox => this.FindControl<NumberBox>("GridEdgeInsetNumberBox");
private TextBlock? GridEdgeInsetComputedPxTextBlock => this.FindControl<TextBlock>("GridEdgeInsetComputedPxTextBlock");
private TextBlock? GridInfoTextBlock => this.FindControl<TextBlock>("GridInfoTextBlock");
private ComboBox? GridSpacingPresetComboBox => this.FindControl<ComboBox>("GridSpacingPresetComboBox");
private Border? GridPreviewHost => this.FindControl<Border>("GridPreviewHost");
private Border? GridPreviewFrame => this.FindControl<Border>("GridPreviewFrame");
private Border? GridPreviewViewport => this.FindControl<Border>("GridPreviewViewport");
private Grid? GridPreviewGrid => this.FindControl<Grid>("GridPreviewGrid");
private Canvas? GridPreviewLinesCanvas => this.FindControl<Canvas>("GridPreviewLinesCanvas");
private Border? GridPreviewTopStatusBarHost => this.FindControl<Border>("GridPreviewTopStatusBarHost");
private StackPanel? GridPreviewTopStatusComponentsPanel => this.FindControl<StackPanel>("GridPreviewTopStatusComponentsPanel");
private Border? GridPreviewBottomTaskbarContainer => this.FindControl<Border>("GridPreviewBottomTaskbarContainer");
private StackPanel? GridPreviewBackButtonVisual => this.FindControl<StackPanel>("GridPreviewBackButtonVisual");
private TextBlock? GridPreviewBackButtonTextBlock => this.FindControl<TextBlock>("GridPreviewBackButtonTextBlock");
private StackPanel? GridPreviewComponentLibraryVisual => this.FindControl<StackPanel>("GridPreviewComponentLibraryVisual");
private FluentIcons.Avalonia.FluentIcon? GridPreviewComponentLibraryIcon => this.FindControl<FluentIcons.Avalonia.FluentIcon>("GridPreviewComponentLibraryIcon");
private TextBlock? GridPreviewComponentLibraryTextBlock => this.FindControl<TextBlock>("GridPreviewComponentLibraryTextBlock");
private FluentIcons.Avalonia.SymbolIcon? GridPreviewSettingsButtonIcon => this.FindControl<FluentIcons.Avalonia.SymbolIcon>("GridPreviewSettingsButtonIcon");
private Border? WallpaperPreviewHost => this.FindControl<Border>("WallpaperPreviewHost");
private Border? WallpaperPreviewFrame => this.FindControl<Border>("WallpaperPreviewFrame");
private Border? WallpaperPreviewViewport => this.FindControl<Border>("WallpaperPreviewViewport");
private Grid? WallpaperPreviewGrid => this.FindControl<Grid>("WallpaperPreviewGrid");
private Border? WallpaperPreviewTopStatusBarHost => this.FindControl<Border>("WallpaperPreviewTopStatusBarHost");
private StackPanel? WallpaperPreviewTopStatusComponentsPanel => this.FindControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel");
private Border? WallpaperPreviewBottomTaskbarContainer => this.FindControl<Border>("WallpaperPreviewBottomTaskbarContainer");
private ClockWidget? WallpaperPreviewClockWidget => this.FindControl<ClockWidget>("WallpaperPreviewClockWidget");
private StackPanel? WallpaperPreviewBackButtonVisual => this.FindControl<StackPanel>("WallpaperPreviewBackButtonVisual");
private TextBlock? WallpaperPreviewBackButtonTextBlock => this.FindControl<TextBlock>("WallpaperPreviewBackButtonTextBlock");
private StackPanel? WallpaperPreviewComponentLibraryVisual => this.FindControl<StackPanel>("WallpaperPreviewComponentLibraryVisual");
private TextBlock? WallpaperPreviewComponentLibraryTextBlock => this.FindControl<TextBlock>("WallpaperPreviewComponentLibraryTextBlock");
private FluentIcons.Avalonia.SymbolIcon? WallpaperPreviewSettingsButtonIcon => this.FindControl<FluentIcons.Avalonia.SymbolIcon>("WallpaperPreviewSettingsButtonIcon");
private ComboBox? StatusBarSpacingModeComboBox => this.FindControl<ComboBox>("StatusBarSpacingModeComboBox");
private SettingsExpanderItem? StatusBarSpacingCustomPanel => this.FindControl<SettingsExpanderItem>("StatusBarSpacingCustomPanel");
private Slider? StatusBarSpacingSlider => this.FindControl<Slider>("StatusBarSpacingSlider");
private NumberBox? StatusBarSpacingNumberBox => this.FindControl<NumberBox>("StatusBarSpacingNumberBox");
private TextBlock? StatusBarSpacingComputedPxTextBlock => this.FindControl<TextBlock>("StatusBarSpacingComputedPxTextBlock");
private ComboBox? TimeZoneComboBox => this.FindControl<ComboBox>("TimeZoneComboBox");
private SettingsExpander? LauncherHiddenItemsSettingsExpander => this.FindControl<SettingsExpander>("LauncherHiddenItemsSettingsExpander");
private TextBlock? LauncherHiddenItemsEmptyTextBlock => this.FindControl<TextBlock>("LauncherHiddenItemsEmptyTextBlock");
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
_ = sender;
_ = e;
ScheduleReloadFromExternalSettings();
}
private void ScheduleReloadFromExternalSettings()
{
if (_externalSettingsReloadPending)
{
return;
}
_externalSettingsReloadPending = true;
DispatcherTimer.RunOnce(() =>
{
_externalSettingsReloadPending = false;
ReloadFromPersistedSettings();
}, TimeSpan.FromMilliseconds(120));
}
private void OnNightModeChecked(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
ApplyNightModeState(true, refreshPalettes: true);
SchedulePersistSettings();
}
private void OnNightModeUnchecked(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
ApplyNightModeState(false, refreshPalettes: true);
SchedulePersistSettings();
}
private void InitializeLocalization(string? languageCode)
{
_languageCode = _localizationService.NormalizeLanguageCode(languageCode);
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
private string Lf(string key, string fallback, params object[] args)
{
var template = L(key, fallback);
return string.Format(CultureInfo.CurrentCulture, template, args);
}
private void ApplyLocalization()
{
Title = L("app.title", "LanMountainDesktop");
BackToWindowsTextBlock.Text = L("button.back_to_windows", "Back to Windows");
OpenComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Widgets");
LauncherTitleTextBlock.Text = L("launcher.title", "App Launcher");
LauncherSubtitleTextBlock.Text = OperatingSystem.IsLinux()
? L("launcher.subtitle_linux", "Displays installed apps discovered from Linux desktop entries.")
: L("launcher.subtitle", "Displays all apps and folders based on the Windows Start menu structure.");
UpdateCurrentRenderBackendStatus();
RenderLauncherHiddenItemsList();
}
private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone)
{
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
var sign = offset >= TimeSpan.Zero ? "+" : "-";
var hours = Math.Abs(offset.Hours);
var minutes = Math.Abs(offset.Minutes);
var name = string.IsNullOrWhiteSpace(timeZone.StandardName) ? timeZone.Id : timeZone.StandardName;
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {name}";
}
private void InitializeWeatherSettings(AppSettingsSnapshot snapshot)
{
_weatherLocationMode = string.Equals(snapshot.WeatherLocationMode, "Coordinates", StringComparison.OrdinalIgnoreCase)
? WeatherLocationMode.Coordinates
: WeatherLocationMode.CitySearch;
_weatherLocationKey = snapshot.WeatherLocationKey ?? string.Empty;
_weatherLocationName = snapshot.WeatherLocationName ?? string.Empty;
_weatherLatitude = snapshot.WeatherLatitude;
_weatherLongitude = snapshot.WeatherLongitude;
_weatherAutoRefreshLocation = snapshot.WeatherAutoRefreshLocation;
_weatherExcludedAlertsRaw = snapshot.WeatherExcludedAlerts ?? string.Empty;
_weatherIconPackId = string.IsNullOrWhiteSpace(snapshot.WeatherIconPackId) ? "FluentRegular" : snapshot.WeatherIconPackId;
_weatherNoTlsRequests = snapshot.WeatherNoTlsRequests;
}
private void InitializeAutoStartWithWindowsSetting(AppSettingsSnapshot snapshot)
{
_autoStartWithWindows = snapshot.AutoStartWithWindows;
}
private void InitializeAppRenderModeSetting(AppSettingsSnapshot snapshot)
{
_selectedAppRenderMode = string.IsNullOrWhiteSpace(snapshot.AppRenderMode)
? AppRenderingModeHelper.Default
: snapshot.AppRenderMode;
_runningAppRenderMode = AppRenderingModeHelper.Normalize(snapshot.AppRenderMode);
}
private void InitializeUpdateSettings(AppSettingsSnapshot snapshot)
{
_ = snapshot;
_ = _updateSettingsService.Get();
}
private void InitializeSettingsIcons()
{
}
private static bool TryParseColor(string? colorText, out Color color)
{
if (!string.IsNullOrWhiteSpace(colorText) && Color.TryParse(colorText, out color))
{
return true;
}
color = default;
return false;
}
private void TryRestoreWallpaper(string? savedWallpaperPath)
{
_wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
if (string.IsNullOrWhiteSpace(_wallpaperPath) || !File.Exists(_wallpaperPath))
{
_wallpaperMediaType = WallpaperMediaType.None;
return;
}
var extension = Path.GetExtension(_wallpaperPath);
if (SupportedVideoExtensions.Contains(extension))
{
_wallpaperMediaType = WallpaperMediaType.Video;
_wallpaperVideoPath = _wallpaperPath;
return;
}
if (!SupportedImageExtensions.Contains(extension))
{
_wallpaperMediaType = WallpaperMediaType.None;
_wallpaperPath = null;
return;
}
try
{
using var stream = File.OpenRead(_wallpaperPath);
_wallpaperBitmap = new Bitmap(stream);
_wallpaperMediaType = WallpaperMediaType.Image;
}
catch
{
_wallpaperMediaType = WallpaperMediaType.None;
_wallpaperPath = null;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
}
}
private void ApplyWallpaperBrush()
{
if (_wallpaperMediaType == WallpaperMediaType.Image && _wallpaperBitmap is not null)
{
DesktopWallpaperLayer.Background = new ImageBrush(_wallpaperBitmap)
{
Stretch = Stretch.UniformToFill
};
return;
}
DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? Brushes.Transparent;
}
private void UpdateWallpaperDisplay()
{
ApplyWallpaperBrush();
}
private void StopVideoWallpaper()
{
_wallpaperVideoPath = null;
if (_wallpaperMediaType == WallpaperMediaType.Video)
{
_wallpaperMediaType = WallpaperMediaType.None;
}
}
private double CalculateCurrentBackgroundLuminance()
{
var brush = DesktopWallpaperLayer.Background;
if (brush is SolidColorBrush solid)
{
return CalculateRelativeLuminance(solid.Color);
}
return CalculateRelativeLuminance(_selectedThemeColor);
}
private void ApplyNightModeState(bool enabled, bool refreshPalettes)
{
_isNightMode = enabled;
if (!refreshPalettes)
{
return;
}
var palette = _themeSettingsService.BuildPalette(enabled, _wallpaperPath);
_recommendedColors = palette.RecommendedColors;
_monetColors = palette.MonetColors;
}
private static double CalculateRelativeLuminance(Color color)
{
return CalculateRelativeLuminance(color.R / 255d, color.G / 255d, color.B / 255d);
}
private static double CalculateRelativeLuminance(double red, double green, double blue)
{
static double ToLinear(double value) =>
value <= 0.03928 ? value / 12.92 : Math.Pow((value + 0.055) / 1.055, 2.4);
return 0.2126 * ToLinear(red) + 0.7152 * ToLinear(green) + 0.0722 * ToLinear(blue);
}
private void TriggerAutoUpdateCheckIfEnabled()
{
}
private void PersistSettings()
{
if (_suppressSettingsPersistence)
{
return;
}
try
{
_settingsService.SaveSnapshot(SettingsScope.App, BuildAppSettingsSnapshot());
_componentLayoutStore.SaveLayout(BuildDesktopLayoutSettingsSnapshot());
_settingsService.SaveSnapshot(SettingsScope.Launcher, BuildLauncherSettingsSnapshot());
}
catch (Exception ex)
{
AppLogger.Warn("SettingsRuntime", "Failed to persist settings.", ex);
}
}
private void SchedulePersistSettings(int delayMs = 200)
{
DispatcherTimer.RunOnce(PersistSettings, TimeSpan.FromMilliseconds(Math.Max(0, delayMs)));
}
internal void ReloadFromPersistedSettings()
{
var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var layoutSnapshot = _componentLayoutStore.LoadLayout();
var launcherSnapshot = _settingsService.LoadSnapshot<LauncherSettingsSnapshot>(SettingsScope.Launcher);
_suppressSettingsPersistence = true;
try
{
ApplyTaskbarSettings(snapshot);
InitializeWeatherSettings(snapshot);
InitializeDesktopSurfaceState(layoutSnapshot);
InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeDesktopComponentPlacements(layoutSnapshot);
TryRestoreWallpaper(snapshot.WallpaperPath);
ApplyWallpaperBrush();
RebuildDesktopGrid();
}
finally
{
_suppressSettingsPersistence = false;
}
}
private AppSettingsSnapshot BuildAppSettingsSnapshot()
{
return new AppSettingsSnapshot
{
GridShortSideCells = _targetShortSideCells,
GridSpacingPreset = _gridSpacingPreset,
DesktopEdgeInsetPercent = _desktopEdgeInsetPercent,
IsNightMode = _isNightMode,
ThemeColor = _selectedThemeColor.ToString(),
WallpaperPath = _wallpaperPath,
LanguageCode = _languageCode,
TimeZoneId = _timeZoneService.CurrentTimeZone.Id,
WeatherLocationMode = _weatherLocationMode.ToString(),
WeatherLocationKey = _weatherLocationKey,
WeatherLocationName = _weatherLocationName,
WeatherLatitude = _weatherLatitude,
WeatherLongitude = _weatherLongitude,
WeatherAutoRefreshLocation = _weatherAutoRefreshLocation,
WeatherExcludedAlerts = _weatherExcludedAlertsRaw,
WeatherIconPackId = _weatherIconPackId,
WeatherNoTlsRequests = _weatherNoTlsRequests,
AutoStartWithWindows = _autoStartWithWindows,
AppRenderMode = _selectedAppRenderMode,
TopStatusComponentIds = [.. _topStatusComponentIds],
PinnedTaskbarActions = [.. _pinnedTaskbarActions.Select(v => v.ToString())],
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
TaskbarLayoutMode = _taskbarLayoutMode,
ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond",
StatusBarSpacingMode = _statusBarSpacingMode,
StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent
};
}
private DesktopLayoutSettingsSnapshot BuildDesktopLayoutSettingsSnapshot()
{
return new DesktopLayoutSettingsSnapshot
{
DesktopPageCount = _desktopPageCount,
CurrentDesktopSurfaceIndex = _currentDesktopSurfaceIndex,
DesktopComponentPlacements = [.. _desktopComponentPlacements]
};
}
private LauncherSettingsSnapshot BuildLauncherSettingsSnapshot()
{
return new LauncherSettingsSnapshot
{
HiddenLauncherAppPaths = [.. _hiddenLauncherAppPaths],
HiddenLauncherFolderPaths = [.. _hiddenLauncherFolderPaths]
};
}
}

View File

@@ -1,493 +0,0 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private const string UpdateChannelStable = "Stable";
private const string UpdateChannelPreview = "Preview";
private bool _autoCheckUpdates = true;
private string _updateChannel = UpdateChannelStable;
private bool _suppressUpdateOptionEvents;
private bool _isCheckingUpdates;
private bool _isDownloadingUpdate;
private string _latestReleaseVersionText = "-";
private DateTimeOffset? _latestReleasePublishedAt;
private string _updateStatusText = string.Empty;
private string _updateDownloadProgressText = string.Empty;
private double _updateDownloadProgressPercent;
private GitHubReleaseAsset? _latestReleaseInstallerAsset;
private string? _downloadedUpdateInstallerPath;
private bool IncludePrereleaseUpdates => string.Equals(
_updateChannel,
UpdateChannelPreview,
StringComparison.OrdinalIgnoreCase);
private void InitializeUpdateSettings(AppSettingsSnapshot snapshot)
{
_autoCheckUpdates = snapshot.AutoCheckUpdates;
_updateChannel = NormalizeUpdateChannel(snapshot.UpdateChannel, snapshot.IncludePrereleaseUpdates);
_latestReleaseVersionText = "-";
_latestReleasePublishedAt = null;
_updateDownloadProgressPercent = 0;
_updateDownloadProgressText = L("settings.update.download_progress_idle", "Download progress: -");
_updateStatusText = L("settings.update.status_ready", "Ready to check for updates.");
_latestReleaseInstallerAsset = null;
_downloadedUpdateInstallerPath = null;
_suppressUpdateOptionEvents = true;
try
{
if (AutoCheckUpdatesToggleSwitch is not null)
{
AutoCheckUpdatesToggleSwitch.IsChecked = _autoCheckUpdates;
}
if (UpdateChannelChipListBox is not null)
{
UpdateChannelChipListBox.SelectedIndex = IncludePrereleaseUpdates ? 1 : 0;
}
}
finally
{
_suppressUpdateOptionEvents = false;
}
UpdateUpdatePanelState();
}
private void TriggerAutoUpdateCheckIfEnabled()
{
if (!_autoCheckUpdates)
{
return;
}
_ = CheckForUpdatesAsync(silentWhenNoUpdate: true);
}
private void ApplyUpdateLocalization()
{
SettingsNavUpdateItem.Content = L("settings.nav.update", "Update");
UpdatePanelTitleTextBlock.Text = L("settings.update.title", "Update");
UpdateCurrentVersionLabelTextBlock.Text = L("settings.update.current_version_label", "Current Version");
UpdateLatestVersionLabelTextBlock.Text = L("settings.update.latest_version_label", "Latest Release");
UpdatePublishedAtLabelTextBlock.Text = L("settings.update.published_at_label", "Published At");
UpdateOptionsSettingsExpander.Header = L("settings.update.options_header", "Update Options");
UpdateOptionsSettingsExpander.Description = L(
"settings.update.options_desc",
"Configure update checks and release channel.");
AutoCheckUpdatesToggleSwitch.Content = L(
"settings.update.auto_check_toggle",
"Automatically check for updates on startup");
UpdateChannelLabelTextBlock.Text = L(
"settings.update.channel_label",
"Update Channel");
UpdateChannelStableChipItem.Content = L(
"settings.update.channel_stable",
"Stable");
UpdateChannelPreviewChipItem.Content = L(
"settings.update.channel_preview",
"Preview");
UpdateActionsSettingsExpander.Header = L("settings.update.actions_header", "Update Actions");
UpdateActionsSettingsExpander.Description = L(
"settings.update.actions_desc",
"Check releases, download installer, and start update.");
CheckForUpdatesButton.Content = L("settings.update.check_button", "Check for Updates");
DownloadAndInstallUpdateButton.Content = L("settings.update.download_install_button", "Download & Install");
if (string.IsNullOrWhiteSpace(_updateDownloadProgressText))
{
_updateDownloadProgressText = L("settings.update.download_progress_idle", "Download progress: -");
}
if (string.IsNullOrWhiteSpace(_updateStatusText))
{
_updateStatusText = L("settings.update.status_ready", "Ready to check for updates.");
}
UpdateUpdatePanelState();
}
private async void OnCheckForUpdatesClick(object? sender, RoutedEventArgs e)
{
await CheckForUpdatesAsync(silentWhenNoUpdate: false);
}
private async void OnDownloadAndInstallUpdateClick(object? sender, RoutedEventArgs e)
{
if (_isCheckingUpdates || _isDownloadingUpdate)
{
return;
}
if (_latestReleaseInstallerAsset is null)
{
await CheckForUpdatesAsync(silentWhenNoUpdate: false);
}
if (_latestReleaseInstallerAsset is null)
{
return;
}
await DownloadAndInstallUpdateAsync(_latestReleaseInstallerAsset);
}
private void OnAutoCheckUpdatesToggled(object? sender, RoutedEventArgs e)
{
if (_suppressUpdateOptionEvents || AutoCheckUpdatesToggleSwitch is null)
{
return;
}
_autoCheckUpdates = AutoCheckUpdatesToggleSwitch.IsChecked == true;
PersistSettings();
}
private void OnUpdateChannelSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressUpdateOptionEvents || UpdateChannelChipListBox is null)
{
return;
}
var selectedChannel = UpdateChannelChipListBox.SelectedIndex == 1
? UpdateChannelPreview
: UpdateChannelStable;
if (string.Equals(_updateChannel, selectedChannel, StringComparison.OrdinalIgnoreCase))
{
return;
}
_updateChannel = selectedChannel;
_latestReleaseInstallerAsset = null;
_latestReleaseVersionText = "-";
_latestReleasePublishedAt = null;
_downloadedUpdateInstallerPath = null;
_updateStatusText = Lf(
"settings.update.status_channel_changed_format",
"Update channel switched to {0}. Please check again.",
GetLocalizedUpdateChannelName(_updateChannel));
PersistSettings();
UpdateUpdatePanelState();
}
private async Task CheckForUpdatesAsync(bool silentWhenNoUpdate)
{
if (_isCheckingUpdates || _isDownloadingUpdate)
{
return;
}
if (!OperatingSystem.IsWindows())
{
_updateStatusText = L(
"settings.update.status_windows_only",
"Automatic installer update is currently available only on Windows.");
UpdateUpdatePanelState();
return;
}
_isCheckingUpdates = true;
_updateStatusText = L("settings.update.status_checking", "Checking GitHub releases...");
_updateDownloadProgressPercent = 0;
_updateDownloadProgressText = L("settings.update.download_progress_idle", "Download progress: -");
UpdateUpdatePanelState();
try
{
if (!Version.TryParse(GetAppVersionText(), out var currentVersion))
{
currentVersion = new Version(0, 0, 0);
}
var result = await _releaseUpdateService.CheckForUpdatesAsync(
currentVersion,
IncludePrereleaseUpdates);
if (!result.Success)
{
_latestReleaseInstallerAsset = null;
_latestReleaseVersionText = "-";
_latestReleasePublishedAt = null;
_downloadedUpdateInstallerPath = null;
_updateStatusText = Lf(
"settings.update.status_check_failed_format",
"Update check failed: {0}",
result.ErrorMessage ?? L("common.unknown", "Unknown error"));
return;
}
_latestReleaseInstallerAsset = result.PreferredAsset;
_latestReleaseVersionText = result.LatestVersionText;
_latestReleasePublishedAt = result.Release?.PublishedAt;
_downloadedUpdateInstallerPath = null;
if (!result.IsUpdateAvailable)
{
_latestReleaseInstallerAsset = null;
_updateStatusText = silentWhenNoUpdate
? L("settings.update.status_up_to_date", "You are already on the latest version.")
: L("settings.update.status_up_to_date", "You are already on the latest version.");
return;
}
if (_latestReleaseInstallerAsset is null)
{
_updateStatusText = L(
"settings.update.status_asset_missing",
"A new release is available, but no compatible installer was found.");
return;
}
_updateStatusText = Lf(
"settings.update.status_available_format",
"New version {0} is available. Click Download & Install.",
_latestReleaseVersionText);
}
catch (Exception ex)
{
_updateStatusText = Lf(
"settings.update.status_check_failed_format",
"Update check failed: {0}",
ex.Message);
}
finally
{
_isCheckingUpdates = false;
UpdateUpdatePanelState();
}
}
private async Task DownloadAndInstallUpdateAsync(GitHubReleaseAsset asset)
{
if (_isCheckingUpdates || _isDownloadingUpdate)
{
return;
}
_isDownloadingUpdate = true;
_updateStatusText = L("settings.update.status_downloading", "Downloading installer...");
_updateDownloadProgressPercent = 0;
_updateDownloadProgressText = Lf(
"settings.update.download_progress_format",
"Download progress: {0:F0}%",
_updateDownloadProgressPercent);
UpdateUpdatePanelState();
try
{
var destinationPath = BuildUpdateInstallerPath(asset.Name);
var progress = new Progress<double>(value =>
{
_updateDownloadProgressPercent = Math.Clamp(value * 100d, 0d, 100d);
_updateDownloadProgressText = Lf(
"settings.update.download_progress_format",
"Download progress: {0:F0}%",
_updateDownloadProgressPercent);
UpdateUpdatePanelState();
});
var result = await _releaseUpdateService.DownloadAssetAsync(asset, destinationPath, progress);
if (!result.Success || string.IsNullOrWhiteSpace(result.FilePath))
{
_updateStatusText = Lf(
"settings.update.status_download_failed_format",
"Download failed: {0}",
result.ErrorMessage ?? L("common.unknown", "Unknown error"));
return;
}
_downloadedUpdateInstallerPath = result.FilePath;
_updateDownloadProgressPercent = 100;
_updateDownloadProgressText = Lf(
"settings.update.download_progress_format",
"Download progress: {0:F0}%",
_updateDownloadProgressPercent);
_updateStatusText = L("settings.update.status_launching_installer", "Download complete. Launching installer...");
UpdateUpdatePanelState();
LaunchInstallerAndExit(_downloadedUpdateInstallerPath);
}
catch (Exception ex)
{
_updateStatusText = Lf(
"settings.update.status_download_failed_format",
"Download failed: {0}",
ex.Message);
}
finally
{
_isDownloadingUpdate = false;
UpdateUpdatePanelState();
}
}
private void LaunchInstallerAndExit(string installerPath)
{
if (string.IsNullOrWhiteSpace(installerPath) || !File.Exists(installerPath))
{
_updateStatusText = L(
"settings.update.status_installer_missing",
"Installer file was not found after download.");
UpdateUpdatePanelState();
return;
}
try
{
Process.Start(new ProcessStartInfo
{
FileName = installerPath,
WorkingDirectory = Path.GetDirectoryName(installerPath) ?? Environment.CurrentDirectory,
UseShellExecute = true,
Verb = "runas"
});
_updateStatusText = L(
"settings.update.status_installer_started",
"Installer started. The app will close for update.");
UpdateUpdatePanelState();
_ = App.CurrentHostApplicationLifecycle?.TryExit(new HostApplicationLifecycleRequest(
Source: nameof(MainWindow),
Reason: "Update installer started successfully."));
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
_updateStatusText = L(
"settings.update.status_elevation_cancelled",
"Administrator permission was not granted. Update was cancelled.");
UpdateUpdatePanelState();
}
catch (Exception ex)
{
_updateStatusText = Lf(
"settings.update.status_launch_failed_format",
"Failed to start installer: {0}",
ex.Message);
UpdateUpdatePanelState();
}
}
private void UpdateUpdatePanelState()
{
if (UpdateCurrentVersionValueTextBlock is not null)
{
UpdateCurrentVersionValueTextBlock.Text = GetAppVersionText();
}
if (UpdateLatestVersionValueTextBlock is not null)
{
UpdateLatestVersionValueTextBlock.Text = string.IsNullOrWhiteSpace(_latestReleaseVersionText)
? "-"
: _latestReleaseVersionText;
}
if (UpdatePublishedAtValueTextBlock is not null)
{
UpdatePublishedAtValueTextBlock.Text = _latestReleasePublishedAt.HasValue &&
_latestReleasePublishedAt.Value != DateTimeOffset.MinValue
? _latestReleasePublishedAt.Value.LocalDateTime.ToString("yyyy-MM-dd HH:mm")
: "-";
}
if (UpdateStatusTextBlock is not null)
{
UpdateStatusTextBlock.Text = string.IsNullOrWhiteSpace(_updateStatusText)
? L("settings.update.status_ready", "Ready to check for updates.")
: _updateStatusText;
}
if (UpdateDownloadProgressTextBlock is not null)
{
UpdateDownloadProgressTextBlock.Text = string.IsNullOrWhiteSpace(_updateDownloadProgressText)
? L("settings.update.download_progress_idle", "Download progress: -")
: _updateDownloadProgressText;
}
if (UpdateDownloadProgressBar is not null)
{
UpdateDownloadProgressBar.IsVisible = _isDownloadingUpdate;
UpdateDownloadProgressBar.Value = Math.Clamp(_updateDownloadProgressPercent, 0d, 100d);
}
if (CheckForUpdatesButton is not null)
{
CheckForUpdatesButton.IsEnabled = !_isCheckingUpdates && !_isDownloadingUpdate;
}
if (DownloadAndInstallUpdateButton is not null)
{
DownloadAndInstallUpdateButton.IsEnabled = !_isCheckingUpdates &&
!_isDownloadingUpdate &&
_latestReleaseInstallerAsset is not null;
}
}
private static string NormalizeUpdateChannel(string? channel, bool includePrereleaseFallback)
{
if (string.Equals(channel, UpdateChannelPreview, StringComparison.OrdinalIgnoreCase))
{
return UpdateChannelPreview;
}
if (string.Equals(channel, UpdateChannelStable, StringComparison.OrdinalIgnoreCase))
{
return UpdateChannelStable;
}
return includePrereleaseFallback ? UpdateChannelPreview : UpdateChannelStable;
}
private string GetLocalizedUpdateChannelName(string channel)
{
return string.Equals(channel, UpdateChannelPreview, StringComparison.OrdinalIgnoreCase)
? L("settings.update.channel_preview", "Preview")
: L("settings.update.channel_stable", "Stable");
}
private static string BuildUpdateInstallerPath(string assetName)
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var updatesDirectory = Path.Combine(appData, "LanMountainDesktop", "Updates");
Directory.CreateDirectory(updatesDirectory);
var safeName = SanitizeFileName(assetName);
return Path.Combine(updatesDirectory, safeName);
}
private static string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
{
return $"LanMountainDesktop-Update-{DateTime.Now:yyyyMMddHHmmss}.exe";
}
var sanitized = fileName;
foreach (var c in Path.GetInvalidFileNameChars())
{
sanitized = sanitized.Replace(c, '_');
}
return sanitized;
}
}

View File

@@ -5,7 +5,6 @@
xmlns:fi="using:FluentIcons.Avalonia" xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ic="using:FluentIcons.Avalonia.Fluent" xmlns:ic="using:FluentIcons.Avalonia.Fluent"
xmlns:comp="using:LanMountainDesktop.Views.Components" xmlns:comp="using:LanMountainDesktop.Views.Components"
xmlns:pages="using:LanMountainDesktop.Views.SettingsPages"
xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia" xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -321,183 +320,6 @@
</Border> </Border>
</Grid> </Grid>
<Grid x:Name="SettingsPage"
Classes="settings-scope"
IsVisible="False"
Opacity="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
</Transitions>
</Grid.Transitions>
<Border x:Name="SettingsContentPanel"
Background="Transparent"
BorderThickness="0"
Margin="0"
Padding="8"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ClipToBounds="False">
<Border.RenderTransform>
<TranslateTransform Y="30">
<TranslateTransform.Transitions>
<Transitions>
<DoubleTransition Property="Y" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
</Transitions>
</TranslateTransform.Transitions>
</TranslateTransform>
</Border.RenderTransform>
<Border Classes="mica-strong"
CornerRadius="{DynamicResource DesignCornerRadiusXl}"
Padding="18">
<Grid RowDefinitions="*,Auto"
RowSpacing="14">
<ui:NavigationView x:Name="SettingsNavView"
Grid.Row="0"
PaneDisplayMode="Left"
IsSettingsVisible="False"
OpenPaneLength="220"
SelectionChanged="OnSettingsNavSelectionChanged">
<ui:NavigationView.MenuItems>
<ui:NavigationViewItem x:Name="SettingsNavWallpaperItem" Content="壁纸" Tag="Wallpaper" ToolTip.Tip="壁纸">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Wallpaper" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavGridItem" Content="网格" Tag="Grid" ToolTip.Tip="网格">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Grid" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavColorItem" Content="颜色" Tag="Color" ToolTip.Tip="颜色">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Color" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavStatusBarItem" Content="状态栏" Tag="StatusBar" ToolTip.Tip="状态栏">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Status" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavWeatherItem" Content="天气" Tag="Weather" ToolTip.Tip="天气">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="WeatherSunny" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavRegionItem" Content="地区" Tag="Region" ToolTip.Tip="地区">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Globe" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavUpdateItem" Content="更新" Tag="Update" ToolTip.Tip="更新">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="ArrowSync" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavAboutItem" Content="关于" Tag="About" ToolTip.Tip="关于">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Info" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavLauncherItem" Content="应用启动台" Tag="Launcher" ToolTip.Tip="应用启动台">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Apps" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavPluginsItem" Content="插件" Tag="Plugins" ToolTip.Tip="插件">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="PuzzlePiece" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavPluginMarketItem" Content="插件市场" Tag="PluginMarket" ToolTip.Tip="插件市场">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="PuzzlePiece" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
</ui:NavigationView.MenuItems>
<ScrollViewer x:Name="SettingsContentScrollViewer"
Padding="0,0,16,0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Grid x:Name="SettingsContentPagesHost">
<pages:WallpaperSettingsPage x:Name="WallpaperSettingsPanel" IsVisible="True" />
<pages:GridSettingsPage x:Name="GridSettingsPanel" IsVisible="False" />
<pages:ColorSettingsPage x:Name="ColorSettingsPanel" IsVisible="False" />
<pages:StatusBarSettingsPage x:Name="StatusBarSettingsPanel" IsVisible="False" />
<pages:WeatherSettingsPage x:Name="WeatherSettingsPanel" IsVisible="False" />
<pages:RegionSettingsPage x:Name="RegionSettingsPanel" IsVisible="False" />
<pages:UpdateSettingsPage x:Name="UpdateSettingsPanel" IsVisible="False" />
<pages:LauncherSettingsPage x:Name="LauncherSettingsPanel" IsVisible="False" />
<pages:AboutSettingsPage x:Name="AboutSettingsPanel" IsVisible="False" />
<pages:PluginSettingsPage x:Name="PluginSettingsPanel" IsVisible="False" />
<pages:PluginMarketSettingsPage x:Name="PluginMarketSettingsPanel" IsVisible="False" />
</Grid>
</ScrollViewer>
</ui:NavigationView>
<StackPanel Grid.Row="1"
Spacing="12">
<Border x:Name="PendingRestartDock"
IsVisible="False"
Classes="glass-panel"
CornerRadius="18"
Padding="14,12">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="12">
<Border Width="34"
Height="34"
CornerRadius="17"
Background="{DynamicResource AdaptiveAccentBrush}">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular"
FontSize="16"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="PendingRestartDockTitleTextBlock"
FontSize="13"
FontWeight="SemiBold"
Text="Restart required" />
<TextBlock x:Name="PendingRestartDockDescriptionTextBlock"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Your changes will apply after restarting the app." />
</StackPanel>
<Button x:Name="PendingRestartDockButton"
Grid.Column="2"
Padding="14,8"
Click="OnPendingRestartDockButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular" />
<TextBlock x:Name="PendingRestartDockButtonTextBlock"
VerticalAlignment="Center"
Text="Restart app" />
</StackPanel>
</Button>
</Grid>
</Border>
</StackPanel>
</Grid>
</Border>
</Border>
</Grid>
<Border x:Name="ComponentLibraryWindow" <Border x:Name="ComponentLibraryWindow"
IsVisible="False" IsVisible="False"
Opacity="0" Opacity="0"

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -20,7 +20,9 @@ using Avalonia.Threading;
using FluentAvalonia.Styling; using FluentAvalonia.Styling;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Theme; using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components; using LanMountainDesktop.Views.Components;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
@@ -73,21 +75,26 @@ public partial class MainWindow : Window
[ [
TaskbarActionId.MinimizeToWindows TaskbarActionId.MinimizeToWindows
]; ];
private readonly DesktopGridLayoutService _gridLayoutService = new(); private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly MonetColorService _monetColorService = new(); private readonly IGridSettingsService _gridSettingsService;
private readonly AppSettingsService _appSettingsService = new(); private readonly IThemeAppearanceService _themeSettingsService;
private readonly DesktopLayoutSettingsService _desktopLayoutSettingsService = new(); private readonly IWeatherSettingsService _weatherSettingsService;
private readonly LauncherSettingsService _launcherSettingsService = new(); private readonly IRegionSettingsService _regionSettingsService;
private readonly ComponentSettingsService _componentSettingsService = new(); private readonly IUpdateSettingsService _updateSettingsService;
private readonly ISettingsService _settingsService;
private readonly IComponentLayoutStore _componentLayoutStore = ComponentDomainStorageProvider.Instance;
private readonly IComponentStateStore _componentStateStore = ComponentDomainStorageProvider.Instance;
private readonly IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly TimeZoneService _timeZoneService = new(); private readonly TimeZoneService _timeZoneService;
private readonly WindowsStartupService _windowsStartupService = new(); private readonly WindowsStartupService _windowsStartupService = new();
private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop"); private readonly IWeatherInfoService _weatherDataService;
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService(); private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService(); private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
private readonly ComponentRegistry _componentRegistry; private readonly ComponentRegistry _componentRegistry;
private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry; private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry;
private readonly IComponentLibraryService _componentLibraryService;
private readonly IComponentLibraryWindowService _componentLibraryWindowService = new ComponentLibraryWindowService();
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme; private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<TaskbarActionId> _pinnedTaskbarActions = []; private readonly HashSet<TaskbarActionId> _pinnedTaskbarActions = [];
@@ -171,71 +178,25 @@ public partial class MainWindow : Window
{ {
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService; var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService); _componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_settingsService = _settingsFacade.Settings;
_gridSettingsService = _settingsFacade.Grid;
_themeSettingsService = _settingsFacade.Theme;
_weatherSettingsService = _settingsFacade.Weather;
_regionSettingsService = _settingsFacade.Region;
_updateSettingsService = _settingsFacade.Update;
_timeZoneService = _regionSettingsService.GetTimeZoneService();
_weatherDataService = _weatherSettingsService.GetWeatherInfoService();
InitializeComponent(); InitializeComponent();
InitializePluginSettingsNavigation();
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry( _componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry, _componentRegistry,
pluginRuntimeService); pluginRuntimeService);
_componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry);
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault(); _fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
AppSettingsService.SettingsSaved += OnExternalAppSettingsSaved; _settingsService.Changed += OnSettingsChanged;
LauncherSettingsService.SettingsSaved += OnExternalLauncherSettingsSaved;
PendingRestartStateService.StateChanged += OnPendingRestartStateChanged;
PropertyChanged += OnWindowPropertyChanged; PropertyChanged += OnWindowPropertyChanged;
InitializeDesktopSurfaceSwipeHandlers(); InitializeDesktopSurfaceSwipeHandlers();
InitializeDesktopComponentDragHandlers(); InitializeDesktopComponentDragHandlers();
PickWallpaperButton.Click += OnPickWallpaperClick;
ClearWallpaperButton.Click += OnClearWallpaperClick;
WallpaperPlacementComboBox.SelectionChanged += OnWallpaperPlacementSelectionChanged;
GridSizeSlider.ValueChanged += OnGridSizeSliderChanged;
GridSpacingPresetComboBox.SelectionChanged += OnGridSpacingPresetSelectionChanged;
GridEdgeInsetSlider.ValueChanged += OnGridEdgeInsetSliderChanged;
ApplyGridButton.Click += OnApplyGridSizeClick;
NightModeToggleSwitch.IsCheckedChanged += OnNightModeIsCheckedChanged;
RecommendedColorButton1.Click += OnRecommendedColorClick;
RecommendedColorButton2.Click += OnRecommendedColorClick;
RecommendedColorButton3.Click += OnRecommendedColorClick;
RecommendedColorButton4.Click += OnRecommendedColorClick;
RecommendedColorButton5.Click += OnRecommendedColorClick;
RecommendedColorButton6.Click += OnRecommendedColorClick;
RefreshMonetColorsButton.Click += OnRefreshMonetColorsClick;
MonetColorButton1.Click += OnMonetColorClick;
MonetColorButton2.Click += OnMonetColorClick;
MonetColorButton3.Click += OnMonetColorClick;
MonetColorButton4.Click += OnMonetColorClick;
MonetColorButton5.Click += OnMonetColorClick;
MonetColorButton6.Click += OnMonetColorClick;
StatusBarClockToggleSwitch.IsCheckedChanged += OnStatusBarClockIsCheckedChanged;
ClockFormatHMSSRadio.IsCheckedChanged += OnClockFormatChanged;
ClockFormatHMRadio.IsCheckedChanged += OnClockFormatChanged;
StatusBarSpacingModeComboBox.SelectionChanged += OnStatusBarSpacingModeChanged;
StatusBarSpacingSlider.ValueChanged += OnStatusBarSpacingSliderChanged;
WeatherPreviewButton.Click += OnTestWeatherRequestClick;
WeatherLocationModeComboBox.SelectionChanged += OnWeatherLocationModeSelectionChanged;
WeatherLocationModeChipListBox.SelectionChanged += OnWeatherLocationModeChipSelectionChanged;
WeatherAutoRefreshToggleSwitch.IsCheckedChanged += OnWeatherAutoRefreshToggled;
WeatherSearchButton.Click += OnSearchWeatherCityClick;
WeatherApplyCityButton.Click += OnApplyWeatherCitySelectionClick;
WeatherApplyCoordinatesButton.Click += OnApplyWeatherCoordinatesClick;
WeatherExcludedAlertsTextBox.LostFocus += OnWeatherExcludedAlertsLostFocus;
WeatherIconPackComboBox.SelectionChanged += OnWeatherIconPackSelectionChanged;
WeatherNoTlsToggleSwitch.IsCheckedChanged += OnWeatherNoTlsToggled;
LanguageComboBox.SelectionChanged += OnLanguageSelectionChanged;
TimeZoneComboBox.SelectionChanged += OnTimeZoneSelectionChanged;
AutoCheckUpdatesToggleSwitch.IsCheckedChanged += OnAutoCheckUpdatesToggled;
UpdateChannelChipListBox.SelectionChanged += OnUpdateChannelSelectionChanged;
CheckForUpdatesButton.Click += OnCheckForUpdatesClick;
DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick;
AutoStartWithWindowsToggleSwitch.IsCheckedChanged += OnAutoStartWithWindowsToggled;
AppRenderModeComboBox.SelectionChanged += OnAppRenderModeSelectionChanged;
} }
private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e) private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
@@ -275,9 +236,9 @@ public partial class MainWindow : Window
base.OnOpened(e); base.OnOpened(e);
_suppressSettingsPersistence = true; _suppressSettingsPersistence = true;
var snapshot = _appSettingsService.Load(); var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var desktopLayoutSnapshot = _desktopLayoutSettingsService.Load(); var desktopLayoutSnapshot = _componentLayoutStore.LoadLayout();
var launcherSnapshot = _launcherSettingsService.Load(); var launcherSnapshot = _settingsService.LoadSnapshot<LauncherSettingsSnapshot>(SettingsScope.Launcher);
if (!string.IsNullOrWhiteSpace(snapshot.TimeZoneId)) if (!string.IsNullOrWhiteSpace(snapshot.TimeZoneId))
{ {
@@ -289,47 +250,16 @@ public partial class MainWindow : Window
MinShortSideCells, MinShortSideCells,
MaxShortSideCells); MaxShortSideCells);
_gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(snapshot.GridSpacingPreset); _gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
_suppressGridSpacingEvents = true;
GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
_suppressGridSpacingEvents = false;
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent); _desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_suppressGridInsetEvents = true;
GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
_suppressGridInsetEvents = false;
GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode); _statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
_statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30); _statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
_suppressStatusBarSpacingEvents = true;
StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch
{
"Compact" => 0,
"Custom" => 2,
_ => 1
};
StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
_suppressStatusBarSpacingEvents = false;
StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged;
GridSizeNumberBox.Value = _targetShortSideCells;
GridSizeSlider.Value = _targetShortSideCells;
GridSizeSlider.ValueChanged += OnGridSizeSliderChanged;
GridSizeNumberBox.ValueChanged += OnGridSizeNumberBoxChanged;
RestoreSettingsTabSelection(snapshot);
UpdateSettingsTabContent();
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
_defaultDesktopBackground = DesktopWallpaperLayer.Background; _defaultDesktopBackground = DesktopWallpaperLayer.Background;
ApplyTaskbarSettings(snapshot); ApplyTaskbarSettings(snapshot);
InitializeLocalization(snapshot.LanguageCode); InitializeLocalization(snapshot.LanguageCode);
InitializeWeatherSettings(snapshot); InitializeWeatherSettings(snapshot);
_ = _componentSettingsService.Load();
InitializeAutoStartWithWindowsSetting(snapshot); InitializeAutoStartWithWindowsSetting(snapshot);
InitializeAppRenderModeSetting(snapshot); InitializeAppRenderModeSetting(snapshot);
InitializeUpdateSettings(snapshot); InitializeUpdateSettings(snapshot);
@@ -349,15 +279,8 @@ public partial class MainWindow : Window
_isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold); _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
ApplyNightModeState(_isNightMode, refreshPalettes: true); ApplyNightModeState(_isNightMode, refreshPalettes: true);
_suppressStatusBarToggleEvents = true;
StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
_suppressStatusBarToggleEvents = false;
ApplyLocalization(); ApplyLocalization();
ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor);
_settingsContentPanelTransform = SettingsContentPanel.RenderTransform as TranslateTransform;
DesktopHost.SizeChanged += OnDesktopHostSizeChanged; DesktopHost.SizeChanged += OnDesktopHostSizeChanged;
WallpaperPreviewHost.SizeChanged += OnWallpaperPreviewHostSizeChanged;
GridPreviewHost.SizeChanged += OnGridPreviewHostSizeChanged;
RebuildDesktopGrid(); RebuildDesktopGrid();
LoadLauncherEntriesAsync(); LoadLauncherEntriesAsync();
InitializeTimeZoneSettings(); InitializeTimeZoneSettings();
@@ -384,28 +307,15 @@ public partial class MainWindow : Window
_wallpaperPreviewSnapshotBitmap = null; _wallpaperPreviewSnapshotBitmap = null;
_libVlc?.Dispose(); _libVlc?.Dispose();
_libVlc = null; _libVlc = null;
if (_weatherDataService is IDisposable weatherServiceDisposable)
{
weatherServiceDisposable.Dispose();
}
if (_recommendationInfoService is IDisposable recommendationServiceDisposable) if (_recommendationInfoService is IDisposable recommendationServiceDisposable)
{ {
recommendationServiceDisposable.Dispose(); recommendationServiceDisposable.Dispose();
} }
_releaseUpdateService.Dispose();
_wallpaperBitmap?.Dispose(); _wallpaperBitmap?.Dispose();
_wallpaperBitmap = null; _wallpaperBitmap = null;
AppSettingsService.SettingsSaved -= OnExternalAppSettingsSaved; _settingsService.Changed -= OnSettingsChanged;
LauncherSettingsService.SettingsSaved -= OnExternalLauncherSettingsSaved;
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
PropertyChanged -= OnWindowPropertyChanged; PropertyChanged -= OnWindowPropertyChanged;
DesktopHost.SizeChanged -= OnDesktopHostSizeChanged; DesktopHost.SizeChanged -= OnDesktopHostSizeChanged;
WallpaperPreviewHost.SizeChanged -= OnWallpaperPreviewHostSizeChanged;
GridPreviewHost.SizeChanged -= OnGridPreviewHostSizeChanged;
GridSizeSlider.ValueChanged -= OnGridSizeSliderChanged;
GridSizeNumberBox.ValueChanged -= OnGridSizeNumberBoxChanged;
GridEdgeInsetNumberBox.ValueChanged -= OnGridEdgeInsetNumberBoxChanged;
StatusBarSpacingNumberBox.ValueChanged -= OnStatusBarSpacingNumberBoxChanged;
base.OnClosed(e); base.OnClosed(e);
} }
@@ -434,6 +344,11 @@ public partial class MainWindow : Window
private void OnGridSizeSliderChanged(object? sender, RoutedEventArgs e) private void OnGridSizeSliderChanged(object? sender, RoutedEventArgs e)
{ {
if (GridSizeSlider is null || GridSizeNumberBox is null)
{
return;
}
var sliderValue = (int)Math.Round(GridSizeSlider.Value); var sliderValue = (int)Math.Round(GridSizeSlider.Value);
if (Math.Abs(GridSizeNumberBox.Value - sliderValue) > double.Epsilon) if (Math.Abs(GridSizeNumberBox.Value - sliderValue) > double.Epsilon)
{ {
@@ -444,6 +359,11 @@ public partial class MainWindow : Window
private void OnGridSizeNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) private void OnGridSizeNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
{ {
if (GridSizeSlider is null || GridSizeNumberBox is null)
{
return;
}
var numberBoxValue = (int)Math.Round(GridSizeNumberBox.Value); var numberBoxValue = (int)Math.Round(GridSizeNumberBox.Value);
if (Math.Abs(GridSizeSlider.Value - numberBoxValue) > double.Epsilon) if (Math.Abs(GridSizeSlider.Value - numberBoxValue) > double.Epsilon)
{ {
@@ -454,6 +374,11 @@ public partial class MainWindow : Window
private void OnGridEdgeInsetSliderChanged(object? sender, RoutedEventArgs e) private void OnGridEdgeInsetSliderChanged(object? sender, RoutedEventArgs e)
{ {
if (GridEdgeInsetSlider is null)
{
return;
}
if (_suppressGridInsetEvents) if (_suppressGridInsetEvents)
{ {
return; return;
@@ -466,6 +391,11 @@ public partial class MainWindow : Window
private void OnGridEdgeInsetNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) private void OnGridEdgeInsetNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
{ {
if (GridEdgeInsetNumberBox is null)
{
return;
}
if (_suppressGridInsetEvents) if (_suppressGridInsetEvents)
{ {
return; return;
@@ -511,6 +441,11 @@ public partial class MainWindow : Window
private void OnStatusBarSpacingModeChanged(object? sender, SelectionChangedEventArgs e) private void OnStatusBarSpacingModeChanged(object? sender, SelectionChangedEventArgs e)
{ {
if (StatusBarSpacingModeComboBox is null)
{
return;
}
if (_suppressStatusBarSpacingEvents) if (_suppressStatusBarSpacingEvents)
{ {
return; return;
@@ -529,6 +464,11 @@ public partial class MainWindow : Window
private void OnStatusBarSpacingSliderChanged(object? sender, RangeBaseValueChangedEventArgs e) private void OnStatusBarSpacingSliderChanged(object? sender, RangeBaseValueChangedEventArgs e)
{ {
if (StatusBarSpacingSlider is null)
{
return;
}
if (_suppressStatusBarSpacingEvents) if (_suppressStatusBarSpacingEvents)
{ {
return; return;
@@ -549,6 +489,11 @@ public partial class MainWindow : Window
private void OnStatusBarSpacingNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) private void OnStatusBarSpacingNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
{ {
if (StatusBarSpacingNumberBox is null)
{
return;
}
if (_suppressStatusBarSpacingEvents) if (_suppressStatusBarSpacingEvents)
{ {
return; return;
@@ -597,7 +542,8 @@ public partial class MainWindow : Window
GridPreviewHost is null || GridPreviewHost is null ||
GridPreviewViewport is null || GridPreviewViewport is null ||
GridPreviewGrid is null || GridPreviewGrid is null ||
GridPreviewLinesCanvas is null) GridPreviewLinesCanvas is null ||
GridSizeSlider is null)
{ {
return; return;
} }
@@ -626,11 +572,11 @@ public partial class MainWindow : Window
var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding); var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding);
var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding); var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding);
var preset = _gridLayoutService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); var preset = _gridSettingsService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
var gapRatio = _gridLayoutService.ResolveGapRatio(preset); var gapRatio = _gridSettingsService.ResolveGapRatio(preset);
var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent(); var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent); var edgeInset = _gridSettingsService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent);
var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset); var gridMetrics = _gridSettingsService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0) if (gridMetrics.CellSize <= 0)
{ {
return; return;
@@ -676,12 +622,15 @@ public partial class MainWindow : Window
ApplyStatusBarComponentSpacingForPanel(GridPreviewTopStatusComponentsPanel, gridMetrics.CellSize); ApplyStatusBarComponentSpacingForPanel(GridPreviewTopStatusComponentsPanel, gridMetrics.CellSize);
UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize); UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize);
GridInfoTextBlock.Text = Lf( if (GridInfoTextBlock is not null)
"settings.grid.info_format", {
"Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", GridInfoTextBlock.Text = Lf(
gridMetrics.ColumnCount, "settings.grid.info_format",
gridMetrics.RowCount, "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
gridMetrics.CellSize); gridMetrics.ColumnCount,
gridMetrics.RowCount,
gridMetrics.CellSize);
}
DrawGridPreviewLines(gridMetrics); DrawGridPreviewLines(gridMetrics);
} }
@@ -767,7 +716,12 @@ public partial class MainWindow : Window
private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e) private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e)
{ {
_gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset( if (GridSizeNumberBox is null || GridSizeSlider is null)
{
return;
}
_gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(
TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
_desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent(); _desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
@@ -825,9 +779,9 @@ public partial class MainWindow : Window
{ {
var hostWidth = DesktopHost.Bounds.Width; var hostWidth = DesktopHost.Bounds.Width;
var hostHeight = DesktopHost.Bounds.Height; var hostHeight = DesktopHost.Bounds.Height;
var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset); var gapRatio = _gridSettingsService.ResolveGapRatio(_gridSpacingPreset);
var edgeInset = _gridLayoutService.CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent); var edgeInset = _gridSettingsService.CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
var gridMetrics = _gridLayoutService.CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset); var gridMetrics = _gridSettingsService.CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0) if (gridMetrics.CellSize <= 0)
{ {
return; return;
@@ -875,12 +829,15 @@ public partial class MainWindow : Window
UpdateDesktopSurfaceLayout(gridMetrics); UpdateDesktopSurfaceLayout(gridMetrics);
UpdateSettingsViewportInsets(gridMetrics.CellSize); UpdateSettingsViewportInsets(gridMetrics.CellSize);
GridInfoTextBlock.Text = Lf( if (GridInfoTextBlock is not null)
"settings.grid.info_format", {
"Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", GridInfoTextBlock.Text = Lf(
gridMetrics.ColumnCount, "settings.grid.info_format",
gridMetrics.RowCount, "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
gridMetrics.CellSize); gridMetrics.ColumnCount,
gridMetrics.RowCount,
gridMetrics.CellSize);
}
UpdateWallpaperPreviewLayout(); UpdateWallpaperPreviewLayout();
} }
@@ -930,6 +887,11 @@ public partial class MainWindow : Window
private int ResolvePendingGridEdgeInsetPercent() private int ResolvePendingGridEdgeInsetPercent()
{ {
if (GridEdgeInsetNumberBox is null)
{
return _desktopEdgeInsetPercent;
}
var pending = (int)Math.Round(GridEdgeInsetNumberBox.Value); var pending = (int)Math.Round(GridEdgeInsetNumberBox.Value);
return Math.Clamp(pending, MinEdgeInsetPercent, MaxEdgeInsetPercent); return Math.Clamp(pending, MinEdgeInsetPercent, MaxEdgeInsetPercent);
} }
@@ -1067,47 +1029,7 @@ public partial class MainWindow : Window
private void UpdateSettingsViewportInsets(double cellSize) private void UpdateSettingsViewportInsets(double cellSize)
{ {
if (SettingsContentPanel is null) _ = cellSize;
{
return;
}
var clampedCell = Math.Max(1, cellSize);
var horizontalInset = Math.Clamp(clampedCell * 0.45, 12, 64);
var verticalGap = Math.Clamp(clampedCell * 0.16, 6, 18);
var edgeInset = Math.Max(0, _currentDesktopEdgeInset);
var taskbarCellHeight = Math.Clamp(clampedCell * 0.76, 36, 76);
var taskbarPadding = Math.Clamp(taskbarCellHeight * 0.16, 6, 14);
var taskbarVisualHeight = Math.Max(clampedCell, taskbarCellHeight + taskbarPadding * 2);
if (BottomTaskbarContainer is not null && BottomTaskbarContainer.Bounds.Height > 1)
{
taskbarVisualHeight = Math.Max(taskbarVisualHeight, BottomTaskbarContainer.Bounds.Height);
}
var statusBarVisualHeight = clampedCell;
if (TopStatusBarHost is not null && TopStatusBarHost.Bounds.Height > 1)
{
statusBarVisualHeight = Math.Max(statusBarVisualHeight, TopStatusBarHost.Bounds.Height);
}
var topInset = Math.Max(clampedCell + verticalGap, edgeInset + statusBarVisualHeight + verticalGap);
var bottomInset = Math.Max(clampedCell + verticalGap, edgeInset + taskbarVisualHeight + verticalGap);
// Add extra safety margin so rounded panel corners never clip against viewport edges.
var cornerSafetyMargin = Math.Clamp(clampedCell * 0.12, 4, 12);
var inset = new Thickness(
horizontalInset + cornerSafetyMargin,
topInset + cornerSafetyMargin,
horizontalInset + cornerSafetyMargin,
bottomInset + cornerSafetyMargin);
// Keep panel stretched with explicit viewport insets so it never overlaps fixed chrome.
SettingsContentPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
SettingsContentPanel.VerticalAlignment = VerticalAlignment.Stretch;
SettingsContentPanel.Margin = inset;
SettingsContentPanel.Width = double.NaN;
SettingsContentPanel.Height = double.NaN;
} }
private void UpdateWallpaperPreviewLayout() private void UpdateWallpaperPreviewLayout()
@@ -1160,9 +1082,9 @@ public partial class MainWindow : Window
var innerWidth = Math.Max(1, previewWidth - horizontalPadding); var innerWidth = Math.Max(1, previewWidth - horizontalPadding);
var innerHeight = Math.Max(1, previewHeight - verticalPadding); var innerHeight = Math.Max(1, previewHeight - verticalPadding);
var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset); var gapRatio = _gridSettingsService.ResolveGapRatio(_gridSpacingPreset);
var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent); var edgeInset = _gridSettingsService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset); var gridMetrics = _gridSettingsService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0) if (gridMetrics.CellSize <= 0)
{ {
return; return;
@@ -1273,6 +1195,11 @@ public partial class MainWindow : Window
private void InitializeTimeZoneSettings() private void InitializeTimeZoneSettings()
{ {
if (TimeZoneComboBox is null)
{
return;
}
// Populate timezone dropdown items before selecting current timezone. // Populate timezone dropdown items before selecting current timezone.
_suppressTimeZoneSelectionEvents = true; _suppressTimeZoneSelectionEvents = true;
TimeZoneComboBox.Items.Clear(); TimeZoneComboBox.Items.Clear();
@@ -1299,7 +1226,9 @@ public partial class MainWindow : Window
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e) private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
if (_suppressTimeZoneSelectionEvents || TimeZoneComboBox.SelectedItem is not ComboBoxItem item) if (TimeZoneComboBox is null ||
_suppressTimeZoneSelectionEvents ||
TimeZoneComboBox.SelectedItem is not ComboBoxItem item)
{ {
return; return;
} }

View File

@@ -1,80 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage">
<StackPanel x:Name="AboutSettingsPanel" Spacing="20">
<TextBlock x:Name="AboutPanelTitleTextBlock" FontSize="24" FontWeight="SemiBold" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Text="About" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="LanMountainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="Modern desktop shell experience." FontSize="13" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Separator Background="{DynamicResource AdaptiveButtonBorderBrush}" Margin="0,8" />
<TextBlock x:Name="VersionTextBlock" Text="Version: 1.0.0" FontSize="13" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="CodeNameTextBlock" Text="Code Name: Administrate" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveAccentBrush}" />
<TextBlock x:Name="FontInfoTextBlock" Text="Font: MiSans" FontSize="12" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="AboutStartupSettingsExpander"
Header="Windows Startup"
Description="Launch the app automatically when signing in to Windows."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Window" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="AutoStartWithWindowsToggleSwitch" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="AboutRenderModeSettingsExpander"
Header="Rendering Mode"
Description="Choose the rendering backend. Restart the app after changing this option. Unsupported modes fall back to software."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Window" />
</ui:SettingsExpander.IconSource>
<StackPanel Spacing="4"
Margin="0,4,0,0">
<TextBlock x:Name="CurrentRenderBackendLabelTextBlock"
Text="Current actual backend"
FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="CurrentRenderBackendValueTextBlock"
Text="Current backend: Software"
FontSize="13"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="CurrentRenderBackendImplementationTextBlock"
Text="Runtime implementation is unavailable."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<ui:SettingsExpander.Footer>
<ComboBox x:Name="AppRenderModeComboBox"
MinWidth="180"
SelectedIndex="0"
HorizontalAlignment="Right">
<ComboBoxItem Content="Default" Tag="Default" />
<ComboBoxItem Content="Software" Tag="Software" />
<ComboBoxItem Content="angleEgl" Tag="AngleEgl" />
<ComboBoxItem Content="WGL" Tag="Wgl" />
<ComboBoxItem Content="Vulkan" Tag="Vulkan" />
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class AboutSettingsPage : UserControl
{
public AboutSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,58 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1400"
x:Class="LanMountainDesktop.Views.SettingsPages.AppearanceSettingsPage">
<StackPanel MaxWidth="920"
Spacing="16">
<TextBlock x:Name="AppearancePageSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Personalize wallpaper, desktop grid, and accent colors in one place." />
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="AppearanceWallpaperSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Wallpaper" />
<TextBlock x:Name="AppearanceWallpaperSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Use lightweight thumbnails and asset controls instead of heavy live preview." />
<ContentControl x:Name="WallpaperContentHost" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="AppearanceGridSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Grid" />
<TextBlock x:Name="AppearanceGridSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Tune grid density, spacing, and safe edge inset for the desktop canvas." />
<ContentControl x:Name="GridContentHost" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="AppearanceColorSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Color" />
<TextBlock x:Name="AppearanceColorSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Choose theme mode and accent colors with Fluent-consistent swatches." />
<ContentControl x:Name="ColorContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class AppearanceSettingsPage : UserControl
{
public AppearanceSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,218 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.ColorSettingsPage">
<StackPanel x:Name="ColorSettingsPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="ColorPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Color" />
<TextBlock x:Name="ColorPanelSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Adjust theme mode and accent colors. The desktop shell will reuse these colors consistently." />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="ThemeModeSettingsExpander"
Header="&#26085;&#22812;&#27169;&#24335;"
Description="&#20999;&#25442;&#24212;&#29992;&#30340;&#27973;&#33394;&#25110;&#28145;&#33394;&#20027;&#39064;&#12290;">
<ui:SettingsExpander.IconSource>
<ic:SymbolIconSource Symbol="DarkTheme"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="NightModeToggleSwitch"
OffContent="Day"
OnContent="Night" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="ThemeColorSettingsExpander"
Header="&#20027;&#39064;&#33394;"
Description="&#36873;&#25321;&#24212;&#29992;&#30340;&#20027;&#39064;&#28857;&#32512;&#33394;&#12290;">
<ui:SettingsExpander.IconSource>
<ic:SymbolIconSource Symbol="Color"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<StackPanel Spacing="12">
<TextBlock x:Name="RecommendedColorsLabelTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Recommended Colors" />
<WrapPanel ItemWidth="72"
ItemHeight="56"
Orientation="Horizontal">
<Button x:Name="RecommendedColorButton1"
Width="68"
Height="50"
Padding="8">
<Border x:Name="RecommendedColorSwatch1"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton2"
Width="68"
Height="50"
Padding="8">
<Border x:Name="RecommendedColorSwatch2"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton3"
Width="68"
Height="50"
Padding="8">
<Border x:Name="RecommendedColorSwatch3"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton4"
Width="68"
Height="50"
Padding="8">
<Border x:Name="RecommendedColorSwatch4"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton5"
Width="68"
Height="50"
Padding="8">
<Border x:Name="RecommendedColorSwatch5"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton6"
Width="68"
Height="50"
Padding="8">
<Border x:Name="RecommendedColorSwatch6"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
</WrapPanel>
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<StackPanel Spacing="12">
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="10">
<TextBlock x:Name="SystemMonetColorsLabelTextBlock"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="System Monet Colors" />
<Button x:Name="RefreshMonetColorsButton"
Grid.Column="1"
Padding="10,6"
Content="Refresh" />
</Grid>
<WrapPanel ItemWidth="72"
ItemHeight="56"
Orientation="Horizontal">
<Button x:Name="MonetColorButton1"
Width="68"
Height="50"
Padding="8">
<Border x:Name="MonetColorSwatch1"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton2"
Width="68"
Height="50"
Padding="8">
<Border x:Name="MonetColorSwatch2"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton3"
Width="68"
Height="50"
Padding="8">
<Border x:Name="MonetColorSwatch3"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton4"
Width="68"
Height="50"
Padding="8">
<Border x:Name="MonetColorSwatch4"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton5"
Width="68"
Height="50"
Padding="8">
<Border x:Name="MonetColorSwatch5"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton6"
Width="68"
Height="50"
Padding="8">
<Border x:Name="MonetColorSwatch6"
Width="26"
Height="26"
CornerRadius="12"
BorderThickness="0" />
</Button>
</WrapPanel>
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell"
Padding="16,14">
<TextBlock x:Name="ThemeColorStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
TextWrapping="Wrap"
Text="Theme color is ready." />
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class ColorSettingsPage : UserControl
{
public ColorSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,48 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1200"
x:Class="LanMountainDesktop.Views.SettingsPages.ComponentsSettingsPage">
<StackPanel MaxWidth="920"
Spacing="16">
<TextBlock x:Name="ComponentsPageSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Review available desktop components and configure the status bar area." />
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="ComponentsSummarySectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Component Library" />
<TextBlock x:Name="ComponentsSummarySectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Built-in and plugin-contributed components available to the desktop editor." />
<TextBlock x:Name="ComponentsSummaryTextBlock"
TextWrapping="Wrap"
Text="Loading component catalog..." />
<StackPanel x:Name="ComponentCategoryItemsPanel"
Spacing="4" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="ComponentsStatusBarSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Status Bar" />
<TextBlock x:Name="ComponentsStatusBarSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Clock and status-bar component spacing are managed here." />
<ContentControl x:Name="StatusBarContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class ComponentsSettingsPage : UserControl
{
public ComponentsSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,58 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1400"
x:Class="LanMountainDesktop.Views.SettingsPages.GeneralSettingsPage">
<StackPanel MaxWidth="920"
Spacing="16">
<TextBlock x:Name="GeneralPageSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Manage language, launcher, and weather behavior from the independent settings module." />
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="GeneralRegionSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Region" />
<TextBlock x:Name="GeneralRegionSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Language and time zone settings affect the entire desktop shell." />
<ContentControl x:Name="RegionContentHost" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="GeneralLauncherSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Launcher" />
<TextBlock x:Name="GeneralLauncherSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Restore hidden entries and adjust how the app launcher behaves." />
<ContentControl x:Name="LauncherContentHost" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="GeneralWeatherSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Weather" />
<TextBlock x:Name="GeneralWeatherSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Configure shared weather source, location, and icon style for weather widgets." />
<ContentControl x:Name="WeatherContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class GeneralSettingsPage : UserControl
{
public GeneralSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,175 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
xmlns:comp="using:LanMountainDesktop.Views.Components"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.GridSettingsPage">
<Grid x:Name="GridSettingsPanel"
ColumnDefinitions="280, *"
RowDefinitions="Auto, *">
<TextBlock x:Name="GridPanelTitleTextBlock"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,20"
Text="璋冩暣缃戞牸甯冨眬" />
<!-- Left Column: Grid Preview -->
<Border x:Name="GridPreviewHost"
Grid.Row="1" Grid.Column="0"
Margin="0,0,20,0"
Width="256"
MaxWidth="256"
VerticalAlignment="Top"
HorizontalAlignment="Left">
<Border x:Name="GridPreviewFrame"
HorizontalAlignment="Stretch"
CornerRadius="22"
Background="#FF1A1A1A"
Padding="8">
<Border x:Name="GridPreviewViewport"
ClipToBounds="True"
CornerRadius="14"
Background="#30111827">
<Panel>
<Canvas x:Name="GridPreviewLinesCanvas"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False" />
<Grid x:Name="GridPreviewGrid"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border x:Name="GridPreviewTopStatusBarHost"
Grid.Row="0"
Background="Transparent"
Padding="2">
<StackPanel x:Name="GridPreviewTopStatusComponentsPanel"
Orientation="Horizontal"
Spacing="3">
</StackPanel>
</Border>
<Border x:Name="GridPreviewBottomTaskbarContainer"
Classes="glass-strong"
Grid.Row="1"
Margin="3"
CornerRadius="16"
Padding="2">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="3">
<Border x:Name="GridPreviewTaskbarFixedActionsHost" Grid.Column="0">
<StackPanel x:Name="GridPreviewBackButtonVisual" Orientation="Horizontal" Spacing="3">
<fi:SymbolIcon Classes="icon-s" Symbol="Window" />
<TextBlock x:Name="GridPreviewBackButtonTextBlock" Text="鍥炲埌Windows" VerticalAlignment="Center" />
</StackPanel>
</Border>
<StackPanel x:Name="GridPreviewTaskbarDynamicActionsHost"
Grid.Column="1"
Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="3" />
<Border x:Name="GridPreviewTaskbarSettingsActionHost" Grid.Column="2">
<StackPanel Orientation="Horizontal" Spacing="3">
<StackPanel x:Name="GridPreviewComponentLibraryVisual" IsVisible="False" Orientation="Horizontal" Spacing="3">
<fi:FluentIcon x:Name="GridPreviewComponentLibraryIcon" Classes="icon-s" Icon="Apps" />
<TextBlock x:Name="GridPreviewComponentLibraryTextBlock" Text="Widget library" VerticalAlignment="Center" />
</StackPanel>
<fi:SymbolIcon x:Name="GridPreviewSettingsButtonIcon" Classes="icon-s" Symbol="Settings" />
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
</Panel>
</Border>
</Border>
</Border>
<!-- Right Column: Settings Content -->
<StackPanel Grid.Row="1" Grid.Column="1"
Margin="20,0,0,0"
Spacing="16">
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="GridRowsSettingsExpander" Header="Rows" Description="Adjust the density of the grid">
<ui:SettingsExpander.Footer>
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12" Width="220">
<Slider x:Name="GridSizeSlider"
Grid.Column="0"
Minimum="6"
Maximum="96"
TickFrequency="1"
TickPlacement="None"
Value="12" />
<ui:NumberBox x:Name="GridSizeNumberBox"
Grid.Column="1"
Width="80"
Minimum="6"
Maximum="96"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Value="12" />
</Grid>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="GridSpacingSettingsExpander" Header="Spacing" Description="Adjust the gap between cells">
<ui:SettingsExpander.Footer>
<ComboBox x:Name="GridSpacingPresetComboBox"
Width="120">
<ComboBoxItem x:Name="GridSpacingRelaxedComboBoxItem" Tag="Relaxed" Content="Relaxed" />
<ComboBoxItem x:Name="GridSpacingCompactComboBoxItem" Tag="Compact" Content="Compact" />
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="GridEdgeInsetSettingsExpander" Header="Screen Inset" Description="Adjust margins around the desktop">
<ui:SettingsExpander.Footer>
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12" Width="220">
<Slider x:Name="GridEdgeInsetSlider"
Grid.Column="0"
Minimum="0"
Maximum="30"
TickFrequency="1"
TickPlacement="None"
Value="18" />
<ui:NumberBox x:Name="GridEdgeInsetNumberBox"
Grid.Column="1"
Width="80"
Minimum="0"
Maximum="30"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Value="18" />
</Grid>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<TextBlock x:Name="GridEdgeInsetComputedPxTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text=">= 0 px" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<Button x:Name="ApplyGridButton"
HorizontalAlignment="Stretch"
Padding="0,10"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Content="搴旂敤" />
<TextBlock x:Name="GridInfoTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Grid: - cols x - rows (1:1)" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class GridSettingsPage : UserControl
{
public GridSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,40 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.LauncherSettingsPage">
<StackPanel x:Name="LauncherSettingsPanel" Spacing="16">
<TextBlock x:Name="LauncherSettingsPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="App Launcher" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="LauncherHiddenItemsSettingsExpander"
Header="Hidden Items"
Description="Review hidden launcher entries and show them again."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe71e;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="LauncherHiddenItemsDescriptionTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Right-click an icon in launcher to hide it. Hidden entries appear here." />
<TextBlock x:Name="LauncherHiddenItemsEmptyTextBlock"
IsVisible="False"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="No hidden items." />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class LauncherSettingsPage : UserControl
{
public LauncherSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,50 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.RegionSettingsPage">
<StackPanel x:Name="RegionSettingsPanel"
Spacing="16">
<TextBlock x:Name="RegionPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Region" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="LanguageSettingsExpander"
Header="Language"
Description="Select application language. Changes apply immediately.">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Translate" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox x:Name="LanguageComboBox"
Width="220">
<ComboBoxItem x:Name="LanguageChineseItem" Tag="zh-CN" Content="涓枃" />
<ComboBoxItem x:Name="LanguageEnglishItem" Tag="en-US" Content="English" />
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="TimeZoneSettingsExpander"
Header="Time Zone"
Description="Select a time zone. Clock and calendar widgets will follow this zone.">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Clock" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox x:Name="TimeZoneComboBox"
Width="280" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class RegionSettingsPage : UserControl
{
public RegionSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,91 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.StatusBarSettingsPage">
<StackPanel x:Name="StatusBarSettingsPanel"
Spacing="16">
<TextBlock x:Name="StatusBarPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Status Bar" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="StatusBarClockSettingsExpander"
Header="&#31995;&#32479;&#26102;&#38047;"
Description="&#22312;&#29366;&#24577;&#26639;&#19978;&#26174;&#31034;&#26102;&#38388;&#12290;"
IsExpanded="False">
<ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="StatusBarClockToggleSwitch"
OnContent="On"
OffContent="Off" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem Content="Display Format">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="16">
<RadioButton x:Name="ClockFormatHMSSRadio"
Content="HH:mm:ss"
GroupName="ClockFormat"
Tag="Hms" />
<RadioButton x:Name="ClockFormatHMRadio"
Content="HH:mm"
GroupName="ClockFormat"
Tag="Hm" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="StatusBarSpacingSettingsExpander"
Header="Component spacing"
Description="Adjust spacing between status bar components."
IsExpanded="False">
<ui:SettingsExpander.Footer>
<ComboBox x:Name="StatusBarSpacingModeComboBox"
Width="150">
<ComboBoxItem x:Name="StatusBarSpacingModeCompactItem" Tag="Compact" Content="Compact" />
<ComboBoxItem x:Name="StatusBarSpacingModeRelaxedItem" Tag="Relaxed" Content="Relaxed" />
<ComboBoxItem x:Name="StatusBarSpacingModeCustomItem" Tag="Custom" Content="Custom" />
</ComboBox>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem x:Name="StatusBarSpacingCustomPanel"
Content="Custom spacing"
IsVisible="False">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="12">
<Slider x:Name="StatusBarSpacingSlider"
Width="150"
Minimum="0"
Maximum="30"
TickFrequency="1"
Value="12" />
<ui:NumberBox x:Name="StatusBarSpacingNumberBox"
Width="80"
Minimum="0"
Maximum="30"
Value="12" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<TextBlock x:Name="StatusBarSpacingComputedPxTextBlock"
HorizontalAlignment="Right"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text=">= 0 px" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class StatusBarSettingsPage : UserControl
{
public StatusBarSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,123 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.UpdateSettingsPage">
<StackPanel x:Name="UpdateSettingsPanel"
Spacing="16">
<TextBlock x:Name="UpdatePanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Update" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="20">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" ColumnSpacing="12" RowSpacing="8">
<TextBlock x:Name="UpdateCurrentVersionLabelTextBlock"
Text="Current Version"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateCurrentVersionValueTextBlock"
Grid.Column="1"
Text="-"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="UpdateLatestVersionLabelTextBlock"
Grid.Row="1"
Text="Latest Release"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateLatestVersionValueTextBlock"
Grid.Row="1" Grid.Column="1"
Text="-"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="UpdatePublishedAtLabelTextBlock"
Grid.Row="2"
Text="Published At"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdatePublishedAtValueTextBlock"
Grid.Row="2" Grid.Column="1"
Text="-"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</Grid>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="UpdateOptionsSettingsExpander"
Header="Update Options"
Description="Configure update checks and release channel."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Settings" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<ToggleSwitch x:Name="AutoCheckUpdatesToggleSwitch"
Content="Automatically check for updates on startup" />
<TextBlock x:Name="UpdateChannelLabelTextBlock"
Text="Update Channel"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ListBox x:Name="UpdateChannelChipListBox"
Classes="settings-chip-list">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBoxItem x:Name="UpdateChannelStableChipItem"
Tag="Stable"
Content="Stable" />
<ListBoxItem x:Name="UpdateChannelPreviewChipItem"
Tag="Preview"
Content="Preview" />
</ListBox>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="UpdateActionsSettingsExpander"
Header="Update Actions"
Description="Check releases, download installer, and start update."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ArrowSync" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="CheckForUpdatesButton"
MinWidth="140"
Content="Check for Updates" />
<Button x:Name="DownloadAndInstallUpdateButton"
MinWidth="180"
Content="Download &amp; Install" />
</StackPanel>
<ProgressBar x:Name="UpdateDownloadProgressBar"
Minimum="0"
Maximum="100"
Height="6"
IsVisible="False" />
<TextBlock x:Name="UpdateDownloadProgressTextBlock"
Text="Download progress: -"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateStatusTextBlock"
Text="Ready to check for updates."
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class UpdateSettingsPage : UserControl
{
public UpdateSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,154 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
xmlns:comp="using:LanMountainDesktop.Views.Components"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.WallpaperSettingsPage">
<Grid x:Name="WallpaperSettingsPanel"
ColumnDefinitions="280, *"
RowDefinitions="Auto, *">
<TextBlock x:Name="WallpaperPanelTitleTextBlock"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,20"
Text="Personalize Wallpaper" />
<!-- Left Column: Monitor Preview -->
<Border x:Name="WallpaperPreviewHost"
Grid.Row="1" Grid.Column="0"
Margin="0,0,20,0"
Width="256"
MaxWidth="256"
VerticalAlignment="Top"
HorizontalAlignment="Left">
<!-- Monitor Frame (Bezel) -->
<Border x:Name="WallpaperPreviewFrame"
HorizontalAlignment="Stretch"
CornerRadius="22"
Background="#FF1A1A1A"
Padding="8">
<Border x:Name="WallpaperPreviewViewport"
ClipToBounds="True"
CornerRadius="14"
Background="#30111827">
<Grid>
<Image x:Name="WallpaperPreviewVideoImage"
IsVisible="False"
IsHitTestVisible="False"
Stretch="UniformToFill"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Grid x:Name="WallpaperPreviewGrid"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border x:Name="WallpaperPreviewTopStatusBarHost"
Grid.Row="0"
Background="Transparent"
Padding="2">
<StackPanel x:Name="WallpaperPreviewTopStatusComponentsPanel"
Orientation="Horizontal"
Spacing="3">
<comp:ClockWidget x:Name="WallpaperPreviewClockWidget"
IsVisible="False" />
</StackPanel>
</Border>
<Border x:Name="WallpaperPreviewBottomTaskbarContainer"
Classes="glass-strong"
Grid.Row="1"
Margin="3"
CornerRadius="16"
Padding="2">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="3">
<Border x:Name="WallpaperPreviewTaskbarFixedActionsHost" Grid.Column="0">
<StackPanel x:Name="WallpaperPreviewBackButtonVisual" Orientation="Horizontal" Spacing="3">
<fi:SymbolIcon Classes="icon-s" Symbol="Window" />
<TextBlock x:Name="WallpaperPreviewBackButtonTextBlock" Text="Back to Windows" VerticalAlignment="Center" />
</StackPanel>
</Border>
<StackPanel x:Name="WallpaperPreviewTaskbarDynamicActionsHost"
Grid.Column="1"
Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="3" />
<Border x:Name="WallpaperPreviewTaskbarSettingsActionHost" Grid.Column="2">
<StackPanel Orientation="Horizontal" Spacing="3">
<StackPanel x:Name="WallpaperPreviewComponentLibraryVisual" IsVisible="False" Orientation="Horizontal" Spacing="3">
<fi:FluentIcon Classes="icon-s" Icon="Apps" />
<TextBlock x:Name="WallpaperPreviewComponentLibraryTextBlock" Text="Widget library" VerticalAlignment="Center" />
</StackPanel>
<fi:SymbolIcon x:Name="WallpaperPreviewSettingsButtonIcon" Classes="icon-s" Symbol="Settings" />
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
</Grid>
</Border>
</Border>
</Border>
<!-- Right Column: Settings Content -->
<StackPanel Grid.Row="1" Grid.Column="1"
Margin="20,0,0,0"
Spacing="16">
<StackPanel Spacing="8">
<TextBlock Text="Preview status" FontSize="12" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="WallpaperPathTextBlock"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextTrimming="CharacterEllipsis"
Text="No file selected" />
<TextBlock x:Name="WallpaperStatusTextBlock"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
Text="Ready" />
</StackPanel>
<Separator Background="{DynamicResource SurfaceStrokeColorDefaultBrush}" Height="1" Margin="0,8" />
<TextBlock Text="Choose image or video" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
<Button x:Name="PickWallpaperButton"
Grid.Column="0"
Classes="accent"
HorizontalAlignment="Stretch"
Padding="0,10"
Content="Browse" />
<Button x:Name="ClearWallpaperButton"
Grid.Column="1"
HorizontalAlignment="Stretch"
Padding="0,10"
Content="Reset" />
</Grid>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WallpaperPlacementSettingsExpander"
Header="Placement"
Padding="12,8">
<ui:SettingsExpander.Footer>
<ComboBox x:Name="WallpaperPlacementComboBox"
Width="120">
<ComboBoxItem Content="Fill" />
<ComboBoxItem Content="Fit" />
<ComboBoxItem Content="Stretch" />
<ComboBoxItem Content="Center" />
<ComboBoxItem Content="Tile" />
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class WallpaperSettingsPage : UserControl
{
public WallpaperSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,355 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
mc:Ignorable="d" d:DesignWidth="860" d:DesignHeight="1200"
x:Class="LanMountainDesktop.Views.SettingsPages.WeatherSettingsPage">
<UserControl.Styles>
<Style Selector="StackPanel.weather-settings-root TextBlock.section-eyebrow">
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Style>
<Style Selector="StackPanel.weather-settings-root Border.preview-icon-shell">
<Setter Property="Width" Value="62" />
<Setter Property="Height" Value="62" />
<Setter Property="CornerRadius" Value="18" />
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="Padding" Value="10" />
</Style>
<Style Selector="StackPanel.weather-settings-root Border.settings-note-shell">
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
<Setter Property="Padding" Value="14,12" />
</Style>
<Style Selector="StackPanel.weather-settings-root Border.settings-expander-shell">
<Setter Property="Margin" Value="0" />
</Style>
</UserControl.Styles>
<StackPanel x:Name="WeatherSettingsContentPanel"
Classes="settings-animated-intro weather-settings-root"
Margin="0,0,8,0"
Spacing="12">
<TextBlock x:Name="WeatherPanelTitleTextBlock"
FontSize="28"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Weather" />
<StackPanel Spacing="8">
<TextBlock x:Name="WeatherPreviewSectionTextBlock"
Classes="section-eyebrow"
Text="Weather Preview" />
<Border Classes="settings-expander-shell"
Padding="18,16">
<Grid RowDefinitions="Auto,Auto"
RowSpacing="10">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="14">
<Border Classes="preview-icon-shell">
<Image x:Name="WeatherPreviewIconImage"
Stretch="Uniform" />
</Border>
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Spacing="3">
<TextBlock x:Name="WeatherPreviewTemperatureTextBlock"
FontSize="34"
FontWeight="SemiBold"
Text="--" />
<TextBlock x:Name="WeatherPreviewUpdatedTextBlock"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="-" />
</StackPanel>
<StackPanel Grid.Column="2"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<Button x:Name="WeatherPreviewButton"
Padding="16,8"
Content="Refresh" />
<ui:ProgressRing x:Name="WeatherPreviewProgressRing"
Width="20"
Height="20"
IsActive="True"
IsVisible="False" />
</StackPanel>
</Grid>
<TextBlock x:Name="WeatherPreviewResultTextBlock"
Grid.Row="1"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Use refresh to verify your weather configuration." />
</Grid>
</Border>
</StackPanel>
<Border Background="{DynamicResource SurfaceStrokeColorDefaultBrush}"
Height="1" />
<TextBlock x:Name="WeatherSettingsSectionTextBlock"
Classes="section-eyebrow"
Text="Settings" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherLocationSettingsExpander"
Header="Location Source"
Description="Choose how weather widgets resolve location."
IsExpanded="True">
<ui:SettingsExpander.Footer>
<ListBox x:Name="WeatherLocationModeChipListBox"
Classes="settings-chip-list"
HorizontalAlignment="Right"
SelectionMode="Single">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBoxItem x:Name="WeatherLocationModeCityChipItem"
Tag="CitySearch"
Content="City Search" />
<ListBoxItem x:Name="WeatherLocationModeCoordinatesChipItem"
Tag="Coordinates"
Content="Coordinates" />
</ListBox>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="18">
<StackPanel Spacing="4">
<TextBlock x:Name="WeatherLocationSelectionTitleTextBlock"
FontSize="17"
FontWeight="SemiBold"
Text="City Selection" />
<TextBlock x:Name="WeatherLocationSelectionDescriptionTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Select the current city used for weather queries." />
</StackPanel>
<StackPanel Grid.Column="1"
MaxWidth="420"
HorizontalAlignment="Right"
Spacing="4">
<TextBlock x:Name="WeatherLocationValueTextBlock"
FontSize="17"
FontWeight="SemiBold"
TextAlignment="Right"
TextWrapping="Wrap"
Text="No location selected" />
<TextBlock x:Name="WeatherLocationStatusTextBlock"
FontSize="12"
TextAlignment="Right"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch x:Name="WeatherAutoRefreshToggleSwitch"
Content="Auto refresh location on startup" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ComboBox x:Name="WeatherLocationModeComboBox"
IsVisible="False">
<ComboBoxItem x:Name="WeatherLocationModeCityItem"
Tag="CitySearch"
Content="City Search" />
<ComboBoxItem x:Name="WeatherLocationModeCoordinatesItem"
Tag="Coordinates"
Content="Coordinates" />
</ComboBox>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherCitySearchSettingsExpander"
Header="City Search"
Description="Search cities and apply one weather location."
IsExpanded="True">
<ui:SettingsExpander.Footer>
<Button x:Name="WeatherApplyCityButton"
Padding="14,8"
Content="Apply City" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<StackPanel Spacing="12">
<Grid ColumnDefinitions="*,Auto,Auto"
ColumnSpacing="10">
<TextBox x:Name="WeatherCitySearchTextBox"
Watermark="e.g. Beijing" />
<ui:ProgressRing x:Name="WeatherSearchProgressRing"
Grid.Column="1"
Width="22"
Height="22"
IsActive="True"
IsVisible="False" />
<Button x:Name="WeatherSearchButton"
Grid.Column="2"
Padding="14,8"
Content="Search" />
</Grid>
<ComboBox x:Name="WeatherCityResultsComboBox"
HorizontalAlignment="Stretch"
MinWidth="320" />
<TextBlock x:Name="WeatherSearchStatusTextBlock"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Search by city name and apply one location." />
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherCoordinateSettingsExpander"
Header="Coordinates"
Description="Set latitude/longitude and optional key/name."
IsVisible="False"
IsExpanded="True">
<ui:SettingsExpander.Footer>
<Button x:Name="WeatherApplyCoordinatesButton"
Padding="14,8"
Content="Apply Coordinates" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<StackPanel Spacing="12">
<Grid ColumnDefinitions="*,*"
ColumnSpacing="10">
<ui:NumberBox x:Name="WeatherLatitudeNumberBox"
Grid.Column="0"
Header="Latitude"
Minimum="-90"
Maximum="90"
SpinButtonPlacementMode="Inline"
SmallChange="0.1"
LargeChange="1"
Value="39.9042" />
<ui:NumberBox x:Name="WeatherLongitudeNumberBox"
Grid.Column="1"
Header="Longitude"
Minimum="-180"
Maximum="180"
SpinButtonPlacementMode="Inline"
SmallChange="0.1"
LargeChange="1"
Value="116.4074" />
</Grid>
<TextBox x:Name="WeatherLocationKeyTextBox"
Watermark="Location key (optional)" />
<TextBox x:Name="WeatherLocationNameTextBox"
Watermark="Display name (optional)" />
<TextBlock x:Name="WeatherCoordinateStatusTextBlock"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherAlertFilterSettingsExpander"
Header="Excluded Alerts"
Description="Alerts containing these words will not be shown. One rule per line."
IsExpanded="True">
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="20">
<StackPanel Width="220"
Spacing="4">
<TextBlock x:Name="WeatherAlertListTitleTextBlock"
FontSize="17"
FontWeight="SemiBold"
Text="Exclude List" />
<TextBlock x:Name="WeatherAlertListDescriptionTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="One exclusion rule per line." />
</StackPanel>
<TextBox x:Name="WeatherExcludedAlertsTextBox"
Grid.Column="1"
MinHeight="96"
MaxHeight="220"
HorizontalAlignment="Stretch"
AcceptsReturn="True"
TextWrapping="Wrap"
Watermark="One keyword per line" />
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherNoTlsSettingsExpander"
Header="No TLS Weather Request"
Description="Not recommended. Enable only for incompatible network environments."
IsExpanded="True">
<ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="WeatherNoTlsToggleSwitch"
Content="Allow non-TLS request fallback" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-note-shell">
<TextBlock x:Name="WeatherFooterHintTextBlock"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Desktop weather widgets will reuse the location and alert exclusion settings configured here." />
</Border>
<Grid IsVisible="False">
<ui:SettingsExpander x:Name="WeatherPreviewSettingsExpander"
Header="Weather Preview"
Description="Refresh and verify current weather service status." />
<fi:SymbolIcon x:Name="WeatherPreviewIconSymbol"
Symbol="WeatherSunny"
IconVariant="Regular" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherIconPackSettingsExpander"
Header="Weather Icon Style"
Description="Choose Fluent Icon style for weather symbols.">
<ui:SettingsExpander.Footer>
<ComboBox x:Name="WeatherIconPackComboBox"
Width="220">
<ComboBoxItem x:Name="WeatherIconPackFluentRegularItem"
Tag="FluentRegular"
Content="Fluent Regular" />
<ComboBoxItem x:Name="WeatherIconPackFluentFilledItem"
Tag="FluentFilled"
Content="Fluent Filled" />
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</Grid>
</StackPanel>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class WeatherSettingsPage : UserControl
{
public WeatherSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,9 +0,0 @@
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private void ApplyPluginMarketSettingsLocalization()
{
PluginMarketSettingsPanel.RefreshFromRuntime();
}
}

View File

@@ -1,15 +0,0 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
internal TextBlock PluginSettingsPanelTitleTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginSettingsPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander PluginSystemSettingsExpander => PluginSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("PluginSystemSettingsExpander")!;
internal TextBlock PluginSystemDescriptionTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginSystemDescriptionTextBlock")!;
internal TextBlock PluginSystemStatusTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginSystemStatusTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander InstalledPluginsSettingsExpander => PluginSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("InstalledPluginsSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander ImportPluginPackageSettingsExpander => PluginSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("ImportPluginPackageSettingsExpander")!;
internal TextBlock PluginRestartHintTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginRestartHintTextBlock")!;
internal TextBlock PluginCatalogEmptyTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginCatalogEmptyTextBlock")!;
}

View File

@@ -1,74 +0,0 @@
using System;
using System.Linq;
using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private void InitializePluginSettingsNavigation()
{
// Legacy plugin settings pages are removed in API-only settings mode.
}
private void UpdatePluginSettingsPageVisibility(string? selectedTag)
{
_ = selectedTag;
}
internal void RefreshPluginSettingsNavigation()
{
// Legacy plugin settings pages are removed in API-only settings mode.
}
private string? GetSelectedSettingsTabTag()
{
return (SettingsNavView?.SelectedItem as NavigationViewItem)?.Tag?.ToString();
}
private int ResolveSelectedSettingsTabIndex()
{
if (SettingsNavView?.SelectedItem is null || SettingsNavView.MenuItems is null)
{
return 0;
}
for (var i = 0; i < SettingsNavView.MenuItems.Count; i++)
{
if (ReferenceEquals(SettingsNavView.MenuItems[i], SettingsNavView.SelectedItem))
{
return i;
}
}
return 0;
}
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag))
{
var taggedItem = SettingsNavView.MenuItems
.OfType<NavigationViewItem>()
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), snapshot.SettingsTabTag, StringComparison.OrdinalIgnoreCase));
if (taggedItem is not null)
{
SettingsNavView.SelectedItem = taggedItem;
return;
}
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, SettingsNavView.MenuItems.Count - 1));
if (SettingsNavView.MenuItems[safeIndex] is NavigationViewItem navItem)
{
SettingsNavView.SelectedItem = navItem;
}
}
}

View File

@@ -1,32 +0,0 @@
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private void ApplyPluginSettingsLocalization()
{
PluginSettingsPanelTitleTextBlock.Text = L("settings.plugins.title", "Plugins");
PluginSystemSettingsExpander.Header = L("settings.plugins.runtime_header", "Plugin Runtime");
PluginSystemSettingsExpander.Description = L(
"settings.plugins.runtime_desc",
"Review plugin runtime state and load results.");
PluginSystemDescriptionTextBlock.Text = L(
"settings.plugins.runtime_hint",
"This page shows discovery status, load results, and runtime diagnostics for installed plugins.");
PluginSystemStatusTextBlock.Text = L(
"settings.plugins.runtime_status",
"Plugin runtime status will appear here after plugin discovery completes.");
InstalledPluginsSettingsExpander.Header = L("settings.plugins.installed_header", "Installed Plugins");
InstalledPluginsSettingsExpander.Description = L(
"settings.plugins.installed_desc",
"Review installed plugins and remove them here.");
ImportPluginPackageSettingsExpander.Header = L("settings.plugins.import_header", "Install From Package");
ImportPluginPackageSettingsExpander.Description = L(
"settings.plugins.import_desc",
"Open a .laapp package and stage it into the local plugin directory.");
PluginRestartHintTextBlock.Text = L(
"settings.plugins.restart_hint",
"Plugin installation and deletion changes take effect after restarting the app.");
PluginCatalogEmptyTextBlock.Text = L("settings.plugins.empty", "No plugins found.");
PluginSettingsPanel.RefreshFromRuntime();
}
}

View File

@@ -1,43 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginMarketSettingsPage">
<StackPanel x:Name="PluginMarketPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="PluginMarketPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Plugin Market" />
<TextBlock x:Name="PluginMarketPanelSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Browse plugins from the official LanAirApp source, review package details, and stage installations safely." />
<Border Classes="settings-expander-shell"
Padding="16,14">
<StackPanel Spacing="10">
<TextBlock x:Name="PluginMarketSectionTitleTextBlock"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Official Source" />
<TextBlock x:Name="PluginMarketSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="The content below is loaded from the official market source. If network loading fails, the module will keep the page alive and show a recoverable error state instead of crashing." />
<ContentControl x:Name="PluginMarketContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,72 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginMarketSettingsPage : UserControl
{
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private PluginMarketEmbeddedView? _pluginMarketView;
public PluginMarketSettingsPage()
{
InitializeComponent();
AttachedToVisualTree += (_, _) => RefreshFromRuntime();
}
public void RefreshFromRuntime()
{
PluginMarketPanelTitleTextBlock.Text = L("settings.plugin_market.title", "Plugin Market");
PluginMarketPanelSubtitleTextBlock.Text = L(
"settings.plugin_market.subtitle",
"Browse plugins from the official LanAirApp source and stage installs.");
var runtime = (Application.Current as App)?.PluginRuntimeService;
if (runtime is null)
{
PluginMarketContentHost.Content = CreateUnavailableState();
return;
}
if (_pluginMarketView is null)
{
_pluginMarketView = new PluginMarketEmbeddedView(runtime);
}
_pluginMarketView.RefreshLocalization();
_pluginMarketView.RefreshInstalledSnapshot();
if (!ReferenceEquals(PluginMarketContentHost.Content, _pluginMarketView))
{
PluginMarketContentHost.Content = _pluginMarketView;
}
}
private Control CreateUnavailableState()
{
return new Border
{
Background = new SolidColorBrush(Color.Parse("#14000000")),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(16),
Child = new TextBlock
{
Text = L(
"settings.plugin_market.unavailable",
"Plugin runtime is not available, so the official market cannot be opened right now."),
TextWrapping = TextWrapping.Wrap,
Foreground = PluginMarketPanelSubtitleTextBlock.Foreground
}
};
}
private string L(string key, string fallback)
{
var snapshot = _appSettingsService.Load();
return _localizationService.GetString(snapshot.LanguageCode, key, fallback);
}
}

View File

@@ -24,13 +24,12 @@ public sealed class PluginRuntimeService : IDisposable
private readonly PluginLoaderOptions _loaderOptions; private readonly PluginLoaderOptions _loaderOptions;
private readonly PluginLoader _loader; private readonly PluginLoader _loader;
private readonly AppSettingsService _appSettingsService = new();
private readonly IHostApplicationLifecycle _applicationLifecycle = new HostApplicationLifecycleService(); private readonly IHostApplicationLifecycle _applicationLifecycle = new HostApplicationLifecycleService();
private readonly PluginExportRegistry _exportRegistry = new(); private readonly PluginExportRegistry _exportRegistry = new();
private readonly PluginSharedContractManager _sharedContractManager; private readonly PluginSharedContractManager _sharedContractManager;
private readonly IServiceProvider _hostServices; private readonly IServiceProvider _hostServices;
private readonly IPluginPackageManager _packageManager; private readonly IPluginPackageManager _packageManager;
private readonly SettingsFacadeService _settingsFacade; private readonly ISettingsFacadeService _settingsFacade;
private readonly SettingsCatalogService _settingsCatalogService; private readonly SettingsCatalogService _settingsCatalogService;
private readonly List<LoadedPlugin> _loadedPlugins = []; private readonly List<LoadedPlugin> _loadedPlugins = [];
private readonly List<PluginLoadResult> _loadResults = []; private readonly List<PluginLoadResult> _loadResults = [];
@@ -39,14 +38,19 @@ public sealed class PluginRuntimeService : IDisposable
private readonly List<PluginDesktopComponentContribution> _desktopComponents = []; private readonly List<PluginDesktopComponentContribution> _desktopComponents = [];
private readonly object _packageMutationGate = new(); private readonly object _packageMutationGate = new();
public PluginRuntimeService() public PluginRuntimeService(ISettingsFacadeService? settingsFacade = null)
{ {
PluginsDirectory = Path.Combine(AppContext.BaseDirectory, "Extensions", "Plugins"); PluginsDirectory = Path.Combine(AppContext.BaseDirectory, "Extensions", "Plugins");
_sharedContractManager = new PluginSharedContractManager( _sharedContractManager = new PluginSharedContractManager(
Path.Combine(GetUserDataRootDirectory(), "PluginMarket")); Path.Combine(GetUserDataRootDirectory(), "PluginMarket"));
_packageManager = new PluginRuntimePackageManager(this); _packageManager = new PluginRuntimePackageManager(this);
_settingsFacade = new SettingsFacadeService(this); _settingsFacade = settingsFacade ?? new SettingsFacadeService();
_settingsCatalogService = (SettingsCatalogService)_settingsFacade.Catalog; _settingsCatalogService = _settingsFacade.Catalog as SettingsCatalogService
?? new SettingsCatalogService();
if (_settingsFacade is SettingsFacadeService concreteFacade)
{
concreteFacade.BindPluginRuntime(this);
}
_hostServices = new PluginHostServiceProvider( _hostServices = new PluginHostServiceProvider(
_packageManager, _packageManager,
_applicationLifecycle, _applicationLifecycle,
@@ -81,7 +85,7 @@ public sealed class PluginRuntimeService : IDisposable
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'."); AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
var disabledPluginIds = GetDisabledPluginIds(); var disabledPluginIds = GetDisabledPluginIds();
var settingsSnapshot = _appSettingsService.Load(); var settingsSnapshot = LoadAppSettingsSnapshot();
var hostLanguageCode = PluginLocalizer.NormalizeLanguageCode(settingsSnapshot.LanguageCode); var hostLanguageCode = PluginLocalizer.NormalizeLanguageCode(settingsSnapshot.LanguageCode);
var hostProperties = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase) var hostProperties = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{ {
@@ -222,7 +226,7 @@ public sealed class PluginRuntimeService : IDisposable
return false; return false;
} }
var snapshot = _appSettingsService.Load(); var snapshot = LoadAppSettingsSnapshot();
var disabledPluginIds = snapshot.DisabledPluginIds is { Count: > 0 } var disabledPluginIds = snapshot.DisabledPluginIds is { Count: > 0 }
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase) ? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(StringComparer.OrdinalIgnoreCase); : new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -239,7 +243,7 @@ public sealed class PluginRuntimeService : IDisposable
snapshot.DisabledPluginIds = disabledPluginIds snapshot.DisabledPluginIds = disabledPluginIds
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase) .OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
_appSettingsService.Save(snapshot); SaveAppSettingsSnapshot(snapshot);
PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true); PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true);
for (var i = 0; i < _catalog.Count; i++) for (var i = 0; i < _catalog.Count; i++)
@@ -386,7 +390,10 @@ public sealed class PluginRuntimeService : IDisposable
{ {
UnloadInstalledPlugins(); UnloadInstalledPlugins();
_sharedContractManager.Dispose(); _sharedContractManager.Dispose();
_settingsFacade.Dispose(); if (_settingsFacade is IDisposable disposable && !ReferenceEquals(_settingsFacade, HostSettingsFacadeProvider.GetOrCreate()))
{
disposable.Dispose();
}
} }
private void UnloadInstalledPlugins() private void UnloadInstalledPlugins()
@@ -409,7 +416,7 @@ public sealed class PluginRuntimeService : IDisposable
private HashSet<string> GetDisabledPluginIds() private HashSet<string> GetDisabledPluginIds()
{ {
var snapshot = _appSettingsService.Load(); var snapshot = LoadAppSettingsSnapshot();
return snapshot.DisabledPluginIds is { Count: > 0 } return snapshot.DisabledPluginIds is { Count: > 0 }
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase) ? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(StringComparer.OrdinalIgnoreCase); : new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -781,13 +788,23 @@ public sealed class PluginRuntimeService : IDisposable
private void RemovePluginFromSnapshot(string pluginId) private void RemovePluginFromSnapshot(string pluginId)
{ {
var snapshot = _appSettingsService.Load(); var snapshot = LoadAppSettingsSnapshot();
if (snapshot.DisabledPluginIds.RemoveAll(id => string.Equals(id, pluginId, StringComparison.OrdinalIgnoreCase)) > 0) if (snapshot.DisabledPluginIds.RemoveAll(id => string.Equals(id, pluginId, StringComparison.OrdinalIgnoreCase)) > 0)
{ {
_appSettingsService.Save(snapshot); SaveAppSettingsSnapshot(snapshot);
} }
} }
private AppSettingsSnapshot LoadAppSettingsSnapshot()
{
return _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
}
private void SaveAppSettingsSnapshot(AppSettingsSnapshot snapshot)
{
_settingsFacade.Settings.SaveSnapshot(SettingsScope.App, snapshot);
}
private void RemovePluginFromCatalog(string pluginId) private void RemovePluginFromCatalog(string pluginId)
{ {
_catalog.RemoveAll(entry => string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase)); _catalog.RemoveAll(entry => string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
internal sealed class PluginScopedSettingsService : IPluginSettingsService
{
private readonly ISettingsService _settingsService;
public PluginScopedSettingsService(string pluginId, ISettingsService settingsService)
{
PluginId = string.IsNullOrWhiteSpace(pluginId) ? "__unknown__" : pluginId.Trim();
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public string PluginId { get; }
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
{
return new ScopedComponentAccessor(this, _settingsService.GetComponentAccessor(componentId, placementId));
}
public T LoadComponentSection<T>(string componentId, string? placementId, string sectionId) where T : new()
{
return _settingsService.LoadSection<T>(
SettingsScope.ComponentInstance,
componentId,
BuildScopedSectionId(sectionId),
placementId);
}
public void SaveComponentSection<T>(
string componentId,
string? placementId,
string sectionId,
T section,
IReadOnlyCollection<string>? changedKeys = null)
{
_settingsService.SaveSection(
SettingsScope.ComponentInstance,
componentId,
BuildScopedSectionId(sectionId),
section,
placementId,
changedKeys);
}
public void DeleteComponentSection(string componentId, string? placementId, string sectionId)
{
_settingsService.DeleteSection(
SettingsScope.ComponentInstance,
componentId,
BuildScopedSectionId(sectionId),
placementId);
}
private string BuildScopedSectionId(string sectionId)
{
var normalizedSectionId = string.IsNullOrWhiteSpace(sectionId) ? "__default__" : sectionId.Trim();
return $"{PluginId}:{normalizedSectionId}";
}
private sealed class ScopedComponentAccessor : IComponentSettingsAccessor
{
private readonly PluginScopedSettingsService _owner;
private readonly IComponentSettingsAccessor _inner;
public ScopedComponentAccessor(PluginScopedSettingsService owner, IComponentSettingsAccessor inner)
{
_owner = owner;
_inner = inner;
}
public string ComponentId => _inner.ComponentId;
public string? PlacementId => _inner.PlacementId;
public T LoadSnapshot<T>() where T : new()
{
return _inner.LoadSnapshot<T>();
}
public void SaveSnapshot<T>(T snapshot, IReadOnlyCollection<string>? changedKeys = null)
{
_inner.SaveSnapshot(snapshot, changedKeys);
}
public T LoadSection<T>(string sectionId) where T : new()
{
return _inner.LoadSection<T>(_owner.BuildScopedSectionId(sectionId));
}
public void SaveSection<T>(string sectionId, T section, IReadOnlyCollection<string>? changedKeys = null)
{
_inner.SaveSection(_owner.BuildScopedSectionId(sectionId), section, changedKeys);
}
public void DeleteSection(string sectionId)
{
_inner.DeleteSection(_owner.BuildScopedSectionId(sectionId));
}
public T? GetValue<T>(string key)
{
return _inner.GetValue<T>($"{_owner.PluginId}:{key}");
}
public void SetValue<T>(string key, T value, IReadOnlyCollection<string>? changedKeys = null)
{
_inner.SetValue($"{_owner.PluginId}:{key}", value, changedKeys);
}
}
}

View File

@@ -1,460 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginSettingsPage : UserControl
{
private static readonly IBrush SuccessBrush = new SolidColorBrush(Color.Parse("#FF0F766E"));
private static readonly IBrush ErrorBrush = new SolidColorBrush(Color.Parse("#FFC42B1C"));
private static readonly IBrush DestructiveBrush = new SolidColorBrush(Color.Parse("#FFF87171"));
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private string? _packageImportStatusMessage;
private bool _packageImportStatusIsError;
public PluginSettingsPage()
{
InitializeComponent();
AttachedToVisualTree += (_, _) => RefreshFromRuntime();
}
public void RefreshFromRuntime()
{
var runtime = (Application.Current as App)?.PluginRuntimeService;
UpdateInstallerUi(runtime);
if (runtime is null)
{
PluginSystemStatusTextBlock.Text = L("settings.plugins.runtime_unavailable", "Plugin runtime is not available.");
PluginRuntimeSummaryPanel.Children.Clear();
InstalledPluginsSettingsExpander.Items.Clear();
PluginRestartHintTextBlock.IsVisible = false;
PluginCatalogEmptyTextBlock.IsVisible = false;
return;
}
BuildRuntimeSummary(runtime);
BuildPluginCatalog(runtime);
}
private void UpdateInstallerUi(PluginRuntimeService? runtime)
{
InstallPluginPackageButton.Content = L("settings.plugins.install_button", "Open .laapp package");
InstallPluginPackageButton.IsEnabled = runtime is not null;
PluginPackageImportHintTextBlock.Text = runtime is null
? L(
"settings.plugins.install_unavailable",
"Plugin runtime is unavailable, so .laapp packages cannot be installed right now.")
: F(
"settings.plugins.install_hint_format",
"Open a .laapp package to install it into: {0}",
runtime.PluginsDirectory);
PluginPackageImportStatusTextBlock.IsVisible = !string.IsNullOrWhiteSpace(_packageImportStatusMessage);
PluginPackageImportStatusTextBlock.Text = _packageImportStatusMessage ?? string.Empty;
PluginPackageImportStatusTextBlock.Foreground = _packageImportStatusIsError ? ErrorBrush : SuccessBrush;
}
private void BuildRuntimeSummary(PluginRuntimeService runtime)
{
var failures = runtime.LoadResults.Where(result => !result.IsSuccess).ToArray();
var enabledCount = runtime.Catalog.Count(entry => entry.IsEnabled);
PluginSystemStatusTextBlock.Text = F(
"settings.plugins.summary_format",
"Detected {0} plugin(s); enabled {1}; loaded {2}; settings sections {3}; widgets {4}; failures {5}.",
runtime.Catalog.Count,
enabledCount,
runtime.Catalog.Count(entry => entry.IsLoaded),
runtime.SettingsSections.Count,
runtime.DesktopComponents.Count,
failures.Length);
PluginRuntimeSummaryPanel.Children.Clear();
foreach (var plugin in runtime.Catalog.OrderBy(entry => entry.Manifest.Name, StringComparer.OrdinalIgnoreCase))
{
var status = plugin.IsEnabled
? plugin.IsLoaded
? L("settings.plugins.state.enabled", "Enabled")
: L("settings.plugins.state.enabled_failed", "Enabled / failed to load")
: L("settings.plugins.state.disabled", "Disabled");
PluginRuntimeSummaryPanel.Children.Add(CreateSummaryLine(
F(
"settings.plugins.summary_item_format",
"{0} v{1} | {2}",
plugin.Manifest.Name,
plugin.Manifest.Version ?? "dev",
status)));
}
}
private void BuildPluginCatalog(PluginRuntimeService runtime)
{
InstalledPluginsSettingsExpander.Items.Clear();
var plugins = runtime.Catalog
.OrderBy(entry => entry.Manifest.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
PluginCatalogEmptyTextBlock.IsVisible = plugins.Count == 0;
PluginRestartHintTextBlock.IsVisible = plugins.Count > 0;
foreach (var plugin in plugins)
{
InstalledPluginsSettingsExpander.Items.Add(CreatePluginCatalogItem(runtime, plugin));
}
}
private SettingsExpanderItem CreatePluginCatalogItem(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
return new SettingsExpanderItem
{
Content = entry.Manifest.Name,
Description = BuildPluginSubtitle(entry),
IconSource = CreatePluginCatalogIconSource(),
IsClickEnabled = false,
Footer = CreatePluginCatalogActions(runtime, entry)
};
}
private void OnInstallPluginPackageClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
UiExceptionGuard.FireAndForgetGuarded(
OnInstallPluginPackageAsync,
"PluginSettings.InstallPackage",
context: "Page=PluginSettings",
onHandledException: ex =>
{
SetPackageImportStatus(
F(
"settings.plugins.install_failed_format",
"Failed to install plugin package: {0}",
ex.Message),
isError: true);
return Task.CompletedTask;
});
}
private async Task OnInstallPluginPackageAsync()
{
var runtime = (Application.Current as App)?.PluginRuntimeService;
if (runtime is null)
{
SetPackageImportStatus(
L(
"settings.plugins.install_unavailable",
"Plugin runtime is unavailable, so .laapp packages cannot be installed right now."),
isError: true);
return;
}
var topLevel = TopLevel.GetTopLevel(this);
var storageProvider = topLevel?.StorageProvider;
if (storageProvider is null)
{
SetPackageImportStatus(
L("settings.plugins.install_picker_unavailable", "Storage provider is unavailable."),
isError: true);
return;
}
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = L("settings.plugins.install_picker_title", "Select plugin package"),
AllowMultiple = false,
FileTypeFilter =
[
new FilePickerFileType(L("settings.plugins.install_file_type", ".laapp plugin package"))
{
Patterns = [$"*{PluginSdkInfo.PackageFileExtension}"]
}
]
});
if (files.Count == 0)
{
return;
}
string? temporaryPackagePath = null;
try
{
temporaryPackagePath = await CopyPackageToTemporaryFileAsync(files[0]);
if (string.IsNullOrWhiteSpace(temporaryPackagePath))
{
SetPackageImportStatus(
L("settings.plugins.install_copy_failed", "Failed to copy the selected .laapp package."),
isError: true);
return;
}
var manifest = runtime.InstallPluginPackage(temporaryPackagePath);
RefreshFromRuntime();
RefreshPluginNavigation(TopLevel.GetTopLevel(this));
SetPackageImportStatus(
F(
"settings.plugins.install_success_format",
"Installed plugin '{0}'. Restart the app to apply newly added settings pages and widgets.",
manifest.Name),
isError: false);
}
catch (Exception ex)
{
SetPackageImportStatus(
F(
"settings.plugins.install_failed_format",
"Failed to install plugin package: {0}",
ex.Message),
isError: true);
}
finally
{
if (!string.IsNullOrWhiteSpace(temporaryPackagePath))
{
try
{
File.Delete(temporaryPackagePath);
}
catch
{
// Ignore temporary file cleanup errors.
}
}
}
}
private void OnDeletePluginClick(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
try
{
if (!runtime.DeleteInstalledPlugin(entry.Manifest.Id))
{
SetPackageImportStatus(
F(
"settings.plugins.delete_failed_format",
"Failed to delete plugin: {0}",
entry.Manifest.Name),
isError: true);
return;
}
RefreshFromRuntime();
RefreshPluginNavigation(TopLevel.GetTopLevel(this));
PluginSystemStatusTextBlock.Text = F(
"settings.plugins.delete_success_format",
"Plugin '{0}' was staged for deletion. Restart the app to finish removing it.",
entry.Manifest.Name);
SetPackageImportStatus(
F(
"settings.plugins.delete_success_format",
"Plugin '{0}' was staged for deletion. Restart the app to finish removing it.",
entry.Manifest.Name),
isError: false);
}
catch (Exception ex)
{
SetPackageImportStatus(
F(
"settings.plugins.delete_failed_detail_format",
"Failed to delete plugin '{0}': {1}",
entry.Manifest.Name,
ex.Message),
isError: true);
}
}
private void OnPluginEnabledChanged(PluginRuntimeService runtime, PluginCatalogEntry entry, bool isEnabled)
{
try
{
if (!runtime.SetPluginEnabled(entry.Manifest.Id, isEnabled))
{
return;
}
RefreshFromRuntime();
var toggleState = isEnabled
? L("settings.plugins.toggle_state_enabled", "enabled")
: L("settings.plugins.toggle_state_disabled", "disabled");
SetPackageImportStatus(
F(
"settings.plugins.toggle_result_format",
"Plugin '{0}' was {1} for the next launch. Restart the app to apply page and widget changes.",
entry.Manifest.Name,
toggleState),
isError: false);
}
catch (Exception ex)
{
SetPackageImportStatus(
F(
"settings.plugins.toggle_failed_detail_format",
"Failed to update plugin '{0}': {1}",
entry.Manifest.Name,
ex.Message),
isError: true);
}
}
private void RefreshPluginNavigation(TopLevel? topLevel)
{
switch (topLevel)
{
case MainWindow mainWindow:
mainWindow.RefreshPluginSettingsNavigation();
break;
}
}
private void SetPackageImportStatus(string message, bool isError)
{
_packageImportStatusMessage = string.IsNullOrWhiteSpace(message) ? null : message;
_packageImportStatusIsError = isError;
UpdateInstallerUi((Application.Current as App)?.PluginRuntimeService);
}
private string BuildPluginSubtitle(PluginCatalogEntry entry)
{
var publisher = string.IsNullOrWhiteSpace(entry.Manifest.Author)
? L("settings.plugins.publisher_unknown", "Unknown publisher")
: entry.Manifest.Author;
return F(
"settings.plugins.publisher_format",
"Publisher: {0}",
publisher);
}
private TextBlock CreateSummaryLine(string text)
{
return new TextBlock
{
Text = text,
TextWrapping = TextWrapping.Wrap,
Foreground = PluginSystemDescriptionTextBlock.Foreground
};
}
private string L(string key, string fallback)
{
var snapshot = _appSettingsService.Load();
return _localizationService.GetString(snapshot.LanguageCode, key, fallback);
}
private string F(string key, string fallback, params object[] args)
{
return string.Format(CultureInfo.CurrentCulture, L(key, fallback), args);
}
private FluentIcons.Avalonia.Fluent.SymbolIconSource CreatePluginCatalogIconSource()
{
return new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = FluentIcons.Common.Symbol.PuzzlePiece,
IconVariant = FluentIcons.Common.IconVariant.Regular
};
}
private Control CreatePluginCatalogActions(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
return new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 10,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
CreateEnablePluginToggle(runtime, entry),
CreateDeletePluginButton(runtime, entry)
}
};
}
private ToggleSwitch CreateEnablePluginToggle(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
var toggle = new ToggleSwitch
{
IsChecked = entry.IsEnabled,
VerticalAlignment = VerticalAlignment.Center
};
ToolTip.SetTip(
toggle,
entry.IsEnabled
? L("settings.plugins.toggle_off", "Disable")
: L("settings.plugins.toggle_on", "Enable"));
toggle.IsCheckedChanged += (_, _) => OnPluginEnabledChanged(runtime, entry, toggle.IsChecked == true);
return toggle;
}
private Button CreateDeletePluginButton(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
var button = new Button
{
Width = 36,
Height = 36,
Padding = new Thickness(0),
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
Content = new FluentIcons.Avalonia.Fluent.SymbolIcon
{
Symbol = FluentIcons.Common.Symbol.Delete,
IconVariant = FluentIcons.Common.IconVariant.Regular,
FontSize = 18,
Foreground = DestructiveBrush,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
ToolTip.SetTip(button, L("settings.plugins.delete_button", "Delete plugin"));
button.Click += (_, _) => OnDeletePluginClick(runtime, entry);
return button;
}
private static async Task<string?> CopyPackageToTemporaryFileAsync(IStorageFile file)
{
try
{
var extension = Path.GetExtension(file.Name);
if (string.IsNullOrWhiteSpace(extension))
{
extension = PluginSdkInfo.PackageFileExtension;
}
var temporaryDirectory = Path.Combine(
Path.GetTempPath(),
"LanMountainDesktop",
"PluginImports");
Directory.CreateDirectory(temporaryDirectory);
var temporaryPackagePath = Path.Combine(
temporaryDirectory,
$"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{extension}");
await using var sourceStream = await file.OpenReadAsync();
await using var destinationStream = File.Create(temporaryPackagePath);
await sourceStream.CopyToAsync(destinationStream);
return temporaryPackagePath;
}
catch
{
return null;
}
}
}

View File

@@ -1,106 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginSettingsPage">
<StackPanel x:Name="PluginSettingsPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="PluginSettingsPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Plugins" />
<TextBlock x:Name="PluginSettingsPanelSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Manage installed plugins, local package import, and runtime availability from one place." />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="PluginSystemSettingsExpander"
Header="Plugin Runtime"
Description="Manage plugin loading and backend isolation."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="PuzzlePiece"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginSystemDescriptionTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="This page will host installed plugin management, permission review, and sandboxed backend runtime controls." />
<Border Background="{DynamicResource LayerFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="14">
<TextBlock x:Name="PluginSystemStatusTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Plugin management UI is not connected yet. Next step is wiring the loader, permissions, and worker isolation state into this panel." />
</Border>
<StackPanel x:Name="PluginRuntimeSummaryPanel" Spacing="6" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="InstalledPluginsSettingsExpander"
Header="Installed Plugins"
Description="Manage installed plugins here."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Apps"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginRestartHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Plugin enable state changes take effect after restarting the app." />
<TextBlock x:Name="PluginCatalogEmptyTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="No plugins found."
IsVisible="False" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="ImportPluginPackageSettingsExpander"
Header="Install From Package"
Description="Open a .laapp package and stage it into the local plugin directory."
IsExpanded="False">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ArrowUpload"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<Button x:Name="InstallPluginPackageButton"
HorizontalAlignment="Left"
Click="OnInstallPluginPackageClick"
Content="Open .laapp package" />
<TextBlock x:Name="PluginPackageImportHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Open a .laapp package to install it into the local plugin directory." />
<TextBlock x:Name="PluginPackageImportStatusTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
IsVisible="False" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -26,8 +26,6 @@
- `PluginLoadContext.cs` - `PluginLoadContext.cs`
- `PluginRuntimeService.cs` - `PluginRuntimeService.cs`
- `PluginCatalogEntry.cs` - `PluginCatalogEntry.cs`
- `PluginSettingsPage.axaml`
- `PluginSettingsPage.Host.cs`
- `PluginMarketIndexService.cs` - `PluginMarketIndexService.cs`
- `PluginMarketInstallService.cs` - `PluginMarketInstallService.cs`