mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 15:44:25 +08:00
setting_re3
This commit is contained in:
21
LanMountainDesktop.PluginSdk/IPluginSettingsService.cs
Normal file
21
LanMountainDesktop.PluginSdk/IPluginSettingsService.cs
Normal 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);
|
||||
}
|
||||
@@ -10,7 +10,8 @@ public sealed class PluginDesktopComponentContext
|
||||
IReadOnlyDictionary<string, object?> properties,
|
||||
string componentId,
|
||||
string? placementId,
|
||||
double cellSize)
|
||||
double cellSize,
|
||||
IPluginSettingsService? pluginSettings = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory);
|
||||
@@ -27,6 +28,7 @@ public sealed class PluginDesktopComponentContext
|
||||
ComponentId = componentId.Trim();
|
||||
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
|
||||
CellSize = Math.Max(1, cellSize);
|
||||
PluginSettings = pluginSettings;
|
||||
}
|
||||
|
||||
public PluginManifest Manifest { get; }
|
||||
@@ -45,6 +47,8 @@ public sealed class PluginDesktopComponentContext
|
||||
|
||||
public double CellSize { get; }
|
||||
|
||||
public IPluginSettingsService? PluginSettings { get; }
|
||||
|
||||
public T? GetService<T>()
|
||||
{
|
||||
return (T?)Services.GetService(typeof(T));
|
||||
|
||||
@@ -10,8 +10,10 @@ using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using AvaloniaWebView;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
using LanMountainDesktop.Views;
|
||||
|
||||
@@ -33,7 +35,7 @@ public partial class App : Application
|
||||
RestartRequested = 2
|
||||
}
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
|
||||
private bool _exitCleanupCompleted;
|
||||
@@ -50,6 +52,7 @@ public partial class App : Application
|
||||
(Current as App)?._hostApplicationLifecycle;
|
||||
|
||||
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
||||
public ISettingsFacadeService SettingsFacade => _settingsFacade;
|
||||
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
||||
|
||||
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
|
||||
@@ -159,7 +162,8 @@ public partial class App : Application
|
||||
try
|
||||
{
|
||||
_pluginRuntimeService?.Dispose();
|
||||
_pluginRuntimeService = new PluginRuntimeService();
|
||||
_pluginRuntimeService = new PluginRuntimeService(_settingsFacade);
|
||||
HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService);
|
||||
_pluginRuntimeService.LoadInstalledPlugins();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -564,7 +568,7 @@ public partial class App : Application
|
||||
|
||||
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);
|
||||
return _localizationService.GetString(languageCode, key, fallback);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public sealed record DesktopComponentRuntimeContext(
|
||||
string ComponentId,
|
||||
string? PlacementId,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
||||
ISettingsService SettingsService,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public interface IComponentSettingsStoreAware
|
||||
{
|
||||
void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore);
|
||||
}
|
||||
@@ -4,7 +4,9 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.WebView.Desktop;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop;
|
||||
|
||||
@@ -121,7 +123,10 @@ sealed class Program
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
114
LanMountainDesktop/Services/ComponentLibraryServices.cs
Normal file
114
LanMountainDesktop/Services/ComponentLibraryServices.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
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 const string LegacySectionId = "__legacy__";
|
||||
private readonly ISettingsService? _settingsService;
|
||||
private readonly IComponentStateStore? _stateStore;
|
||||
private readonly IComponentMessageStore? _messageStore;
|
||||
private string _scopedComponentId = string.Empty;
|
||||
private string _scopedPlacementId = string.Empty;
|
||||
|
||||
public ComponentSettingsService()
|
||||
: this(Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LanMountainDesktop"))
|
||||
{
|
||||
_settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
_settingsPath = Path.Combine(settingsDirectory, "component-settings.json");
|
||||
_legacyAppSettingsPath = Path.Combine(settingsDirectory, "settings.json");
|
||||
var storage = new SqliteComponentDomainStorage(settingsDirectory);
|
||||
_stateStore = storage;
|
||||
_messageStore = storage;
|
||||
}
|
||||
|
||||
public ComponentSettingsSnapshot Load()
|
||||
@@ -55,19 +38,15 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
|
||||
return LoadForComponent(_scopedComponentId, _scopedPlacementId);
|
||||
}
|
||||
|
||||
try
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
lock (CacheGate)
|
||||
{
|
||||
var document = LoadDocumentLocked();
|
||||
return document.DefaultSettings.Clone();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("ComponentSettings", $"Failed to load component settings from '{_settingsPath}'.", ex);
|
||||
return new ComponentSettingsSnapshot();
|
||||
return _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
|
||||
SettingsScope.ComponentInstance,
|
||||
subjectId: string.Empty,
|
||||
placementId: null);
|
||||
}
|
||||
|
||||
return _stateStore?.LoadState(componentId: string.Empty, placementId: null) ?? new ComponentSettingsSnapshot();
|
||||
}
|
||||
|
||||
public void Save(ComponentSettingsSnapshot snapshot)
|
||||
@@ -78,186 +57,116 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshotToPersist = NormalizeSnapshot(snapshot);
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
_settingsService.SaveSnapshot(
|
||||
SettingsScope.ComponentInstance,
|
||||
snapshot ?? new ComponentSettingsSnapshot(),
|
||||
subjectId: string.Empty,
|
||||
placementId: null);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
_stateStore?.SaveState(componentId: string.Empty, placementId: null, snapshot ?? new ComponentSettingsSnapshot());
|
||||
}
|
||||
|
||||
public ComponentSettingsSnapshot LoadForComponent(string componentId, string? placementId)
|
||||
{
|
||||
try
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
lock (CacheGate)
|
||||
{
|
||||
var document = LoadDocumentLocked();
|
||||
var instanceKey = BuildInstanceKey(componentId, placementId);
|
||||
if (!string.IsNullOrWhiteSpace(instanceKey) &&
|
||||
document.InstanceSettings.TryGetValue(instanceKey, out var snapshot))
|
||||
{
|
||||
return snapshot.Clone();
|
||||
}
|
||||
return _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
|
||||
SettingsScope.ComponentInstance,
|
||||
subjectId: componentId,
|
||||
placementId: placementId);
|
||||
}
|
||||
|
||||
return document.DefaultSettings.Clone();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn(
|
||||
"ComponentSettings",
|
||||
$"Failed to load component settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}",
|
||||
ex);
|
||||
return new ComponentSettingsSnapshot();
|
||||
}
|
||||
return _stateStore?.LoadState(componentId, placementId) ?? new ComponentSettingsSnapshot();
|
||||
}
|
||||
|
||||
public void SaveForComponent(string componentId, string? placementId, ComponentSettingsSnapshot snapshot)
|
||||
{
|
||||
var normalizedSnapshot = NormalizeSnapshot(snapshot);
|
||||
var instanceKey = BuildInstanceKey(componentId, placementId);
|
||||
if (string.IsNullOrWhiteSpace(instanceKey))
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
Save(normalizedSnapshot);
|
||||
_settingsService.SaveSnapshot(
|
||||
SettingsScope.ComponentInstance,
|
||||
snapshot ?? new ComponentSettingsSnapshot(),
|
||||
subjectId: componentId,
|
||||
placementId: placementId);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
_stateStore?.SaveState(componentId, placementId, snapshot ?? new ComponentSettingsSnapshot());
|
||||
}
|
||||
|
||||
public void DeleteForComponent(string componentId, string? placementId)
|
||||
{
|
||||
var instanceKey = BuildInstanceKey(componentId, placementId);
|
||||
if (string.IsNullOrWhiteSpace(instanceKey))
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
_settingsService.SaveSnapshot(
|
||||
SettingsScope.ComponentInstance,
|
||||
new ComponentSettingsSnapshot(),
|
||||
subjectId: componentId,
|
||||
placementId: placementId);
|
||||
_settingsService.DeleteSection(SettingsScope.ComponentInstance, componentId, LegacySectionId, placementId);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
_stateStore?.DeleteState(componentId, placementId);
|
||||
}
|
||||
|
||||
public T LoadPluginSettings<T>(string componentId, string? placementId) where T : new()
|
||||
{
|
||||
try
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
lock (CacheGate)
|
||||
{
|
||||
var document = LoadDocumentLocked();
|
||||
var instanceKey = BuildInstanceKey(componentId, placementId);
|
||||
if (string.IsNullOrWhiteSpace(instanceKey) ||
|
||||
!document.PluginSettings.TryGetValue(instanceKey, out var settingsElement))
|
||||
{
|
||||
return new T();
|
||||
}
|
||||
return _settingsService.LoadSection<T>(
|
||||
SettingsScope.ComponentInstance,
|
||||
subjectId: componentId,
|
||||
sectionId: LegacySectionId,
|
||||
placementId: placementId);
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<T>(settingsElement.GetRawText(), SerializerOptions) ?? new T();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
if (_messageStore is SqliteComponentDomainStorage sqliteStorage)
|
||||
{
|
||||
AppLogger.Warn(
|
||||
"ComponentSettings",
|
||||
$"Failed to load plugin settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}",
|
||||
ex);
|
||||
return new T();
|
||||
return sqliteStorage.LoadLegacyMessage<T>(componentId, placementId);
|
||||
}
|
||||
|
||||
return new T();
|
||||
}
|
||||
|
||||
public void SavePluginSettings<T>(string componentId, string? placementId, T settings)
|
||||
{
|
||||
var instanceKey = BuildInstanceKey(componentId, placementId);
|
||||
if (string.IsNullOrWhiteSpace(instanceKey))
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
_settingsService.SaveSection(
|
||||
SettingsScope.ComponentInstance,
|
||||
subjectId: componentId,
|
||||
sectionId: LegacySectionId,
|
||||
section: settings,
|
||||
placementId: placementId);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
if (_messageStore is SqliteComponentDomainStorage sqliteStorage)
|
||||
{
|
||||
lock (CacheGate)
|
||||
{
|
||||
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);
|
||||
sqliteStorage.SaveLegacyMessage(componentId, placementId, settings);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeletePluginSettings(string componentId, string? placementId)
|
||||
{
|
||||
var instanceKey = BuildInstanceKey(componentId, placementId);
|
||||
if (string.IsNullOrWhiteSpace(instanceKey))
|
||||
if (_settingsService is not null)
|
||||
{
|
||||
_settingsService.DeleteSection(
|
||||
SettingsScope.ComponentInstance,
|
||||
subjectId: componentId,
|
||||
sectionId: LegacySectionId,
|
||||
placementId: placementId);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
if (_messageStore is SqliteComponentDomainStorage sqliteStorage)
|
||||
{
|
||||
lock (CacheGate)
|
||||
{
|
||||
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);
|
||||
sqliteStorage.DeleteLegacyMessage(componentId, placementId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) &&
|
||||
_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;
|
||||
// no-op: SQLite storage is directly persisted without in-memory cache.
|
||||
}
|
||||
|
||||
private bool HasScopedComponentContext()
|
||||
@@ -695,192 +228,4 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
|
||||
return !string.IsNullOrWhiteSpace(_scopedComponentId) &&
|
||||
!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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Avalonia.Media;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.ComponentSystem.Extensions;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
@@ -114,6 +115,11 @@ public static class DesktopComponentRegistryFactory
|
||||
{
|
||||
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(
|
||||
contribution.Plugin.Manifest,
|
||||
contribution.Plugin.Context.PluginDirectory,
|
||||
@@ -122,7 +128,8 @@ public static class DesktopComponentRegistryFactory
|
||||
contribution.Plugin.Context.Properties,
|
||||
contribution.Registration.ComponentId,
|
||||
context.PlacementId,
|
||||
context.CellSize);
|
||||
context.CellSize,
|
||||
pluginSettings);
|
||||
|
||||
return contribution.Registration.ControlFactory(contribution.Plugin.Services, pluginContext);
|
||||
}
|
||||
|
||||
@@ -1,251 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class DesktopLayoutSettingsService
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
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");
|
||||
}
|
||||
private readonly IComponentLayoutStore _layoutStore = ComponentDomainStorageProvider.Instance;
|
||||
|
||||
public DesktopLayoutSettingsSnapshot Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
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();
|
||||
}
|
||||
return _layoutStore.LoadLayout();
|
||||
}
|
||||
|
||||
public void Save(DesktopLayoutSettingsSnapshot snapshot)
|
||||
{
|
||||
var snapshotToPersist = NormalizeSnapshot(snapshot);
|
||||
|
||||
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; }
|
||||
_layoutStore.SaveLayout(snapshot ?? new DesktopLayoutSettingsSnapshot());
|
||||
}
|
||||
}
|
||||
|
||||
845
LanMountainDesktop/Services/Settings/ComponentDomainStorage.cs
Normal file
845
LanMountainDesktop/Services/Settings/ComponentDomainStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,15 @@ public interface IGridSettingsService
|
||||
{
|
||||
GridSettingsState Get();
|
||||
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
|
||||
@@ -117,12 +126,14 @@ public interface IWeatherSettingsService
|
||||
{
|
||||
WeatherSettingsState Get();
|
||||
void Save(WeatherSettingsState state);
|
||||
IWeatherInfoService GetWeatherInfoService();
|
||||
}
|
||||
|
||||
public interface IRegionSettingsService
|
||||
{
|
||||
RegionSettingsState Get();
|
||||
void Save(RegionSettingsState state);
|
||||
TimeZoneService GetTimeZoneService();
|
||||
}
|
||||
|
||||
public interface IUpdateSettingsService
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace LanMountainDesktop.Services.Settings;
|
||||
internal sealed class GridSettingsService : IGridSettingsService
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly DesktopGridLayoutService _gridLayoutService = new();
|
||||
|
||||
public GridSettingsState Get()
|
||||
{
|
||||
@@ -32,6 +33,36 @@ internal sealed class GridSettingsService : IGridSettingsService
|
||||
snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent;
|
||||
_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
|
||||
@@ -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();
|
||||
|
||||
@@ -252,11 +283,20 @@ internal sealed class WeatherProviderAdapter : IWeatherProvider
|
||||
{
|
||||
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 WeatherProviderAdapter _weatherProvider = new();
|
||||
|
||||
public WeatherSettingsState Get()
|
||||
{
|
||||
@@ -289,11 +329,22 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService
|
||||
snapshot.WeatherLocationQuery = state.LocationQuery;
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
|
||||
public IWeatherInfoService GetWeatherInfoService()
|
||||
{
|
||||
return _weatherProvider;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_weatherProvider.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RegionSettingsService : IRegionSettingsService
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly TimeZoneService _timeZoneService = new();
|
||||
|
||||
public RegionSettingsState Get()
|
||||
{
|
||||
@@ -312,6 +363,11 @@ internal sealed class RegionSettingsService : IRegionSettingsService
|
||||
: state.TimeZoneId.Trim();
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
|
||||
public TimeZoneService GetTimeZoneService()
|
||||
{
|
||||
return _timeZoneService;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable
|
||||
@@ -388,13 +444,18 @@ internal sealed class LauncherPolicyService : ILauncherPolicyService
|
||||
internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly PluginRuntimeService? _pluginRuntimeService;
|
||||
private PluginRuntimeService? _pluginRuntimeService;
|
||||
|
||||
public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService)
|
||||
{
|
||||
_pluginRuntimeService = pluginRuntimeService;
|
||||
}
|
||||
|
||||
public void SetPluginRuntime(PluginRuntimeService? pluginRuntimeService)
|
||||
{
|
||||
_pluginRuntimeService = pluginRuntimeService;
|
||||
}
|
||||
|
||||
public PluginManagementSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
@@ -426,9 +487,9 @@ internal sealed class PluginManagementSettingsService : IPluginManagementSetting
|
||||
|
||||
internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService, IDisposable
|
||||
{
|
||||
private readonly PluginRuntimeService? _pluginRuntimeService;
|
||||
private readonly AirAppMarketIndexService _indexService;
|
||||
private readonly AirAppMarketInstallService? _installService;
|
||||
private PluginRuntimeService? _pluginRuntimeService;
|
||||
private AirAppMarketIndexService _indexService;
|
||||
private AirAppMarketInstallService? _installService;
|
||||
private readonly Dictionary<string, AirAppMarketPluginEntry> _cachedPlugins = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
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)
|
||||
{
|
||||
var result = await _indexService.LoadAsync(cancellationToken);
|
||||
@@ -567,6 +646,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
||||
{
|
||||
private readonly UpdateSettingsService _updateSettingsService;
|
||||
private readonly PluginMarketSettingsService _pluginMarketSettingsService;
|
||||
private readonly PluginManagementSettingsService _pluginManagementSettingsService;
|
||||
private readonly WeatherSettingsService _weatherSettingsService;
|
||||
|
||||
public SettingsFacadeService(PluginRuntimeService? pluginRuntimeService = null)
|
||||
{
|
||||
@@ -577,13 +658,15 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
||||
WallpaperMedia = new WallpaperMediaService();
|
||||
Theme = new ThemeAppearanceService();
|
||||
StatusBar = new StatusBarSettingsService();
|
||||
Weather = new WeatherSettingsService();
|
||||
_weatherSettingsService = new WeatherSettingsService();
|
||||
Weather = _weatherSettingsService;
|
||||
Region = new RegionSettingsService();
|
||||
_updateSettingsService = new UpdateSettingsService();
|
||||
Update = _updateSettingsService;
|
||||
LauncherCatalog = new LauncherCatalogService();
|
||||
LauncherPolicy = new LauncherPolicyService();
|
||||
PluginManagement = new PluginManagementSettingsService(pluginRuntimeService);
|
||||
_pluginManagementSettingsService = new PluginManagementSettingsService(pluginRuntimeService);
|
||||
PluginManagement = _pluginManagementSettingsService;
|
||||
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
|
||||
PluginMarket = _pluginMarketSettingsService;
|
||||
ApplicationInfo = new ApplicationInfoService();
|
||||
@@ -619,8 +702,15 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
||||
|
||||
public IApplicationInfoService ApplicationInfo { get; }
|
||||
|
||||
public void BindPluginRuntime(PluginRuntimeService? pluginRuntimeService)
|
||||
{
|
||||
_pluginManagementSettingsService.SetPluginRuntime(pluginRuntimeService);
|
||||
_pluginMarketSettingsService.SetPluginRuntime(pluginRuntimeService);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_weatherSettingsService.Dispose();
|
||||
_updateSettingsService.Dispose();
|
||||
_pluginMarketSettingsService.Dispose();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ internal sealed class SettingsService : ISettingsService
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = 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 object _pluginSettingsGate = new();
|
||||
|
||||
@@ -80,7 +81,7 @@ internal sealed class SettingsService : ISettingsService
|
||||
{
|
||||
if (scope == SettingsScope.ComponentInstance)
|
||||
{
|
||||
return _componentSettingsService.LoadPluginSettings<T>(EnsureKey(subjectId), placementId);
|
||||
return _componentMessageStore.LoadSection<T>(EnsureKey(subjectId), placementId, EnsureKey(sectionId));
|
||||
}
|
||||
|
||||
if (scope != SettingsScope.Plugin)
|
||||
@@ -111,7 +112,7 @@ internal sealed class SettingsService : ISettingsService
|
||||
{
|
||||
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));
|
||||
return;
|
||||
}
|
||||
@@ -142,7 +143,7 @@ internal sealed class SettingsService : ISettingsService
|
||||
{
|
||||
if (scope == SettingsScope.ComponentInstance)
|
||||
{
|
||||
_componentSettingsService.DeletePluginSettings(EnsureKey(subjectId), placementId);
|
||||
_componentMessageStore.DeleteSection(EnsureKey(subjectId), placementId, EnsureKey(sectionId));
|
||||
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId));
|
||||
return;
|
||||
}
|
||||
@@ -183,7 +184,11 @@ internal sealed class SettingsService : ISettingsService
|
||||
SettingsScope.App => JsonSerializer.SerializeToElement(_appSettingsService.Load(), SerializerOptions),
|
||||
SettingsScope.Launcher => JsonSerializer.SerializeToElement(_launcherSettingsService.Load(), SerializerOptions),
|
||||
SettingsScope.ComponentInstance => JsonSerializer.SerializeToElement(
|
||||
_componentSettingsService.LoadForComponent(EnsureKey(subjectId), placementId),
|
||||
LoadSection<Dictionary<string, JsonElement>>(
|
||||
SettingsScope.ComponentInstance,
|
||||
EnsureKey(subjectId),
|
||||
sectionId ?? "__root__",
|
||||
placementId),
|
||||
SerializerOptions),
|
||||
SettingsScope.Plugin => JsonSerializer.SerializeToElement(
|
||||
LoadSection<Dictionary<string, JsonElement>>(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", placementId),
|
||||
@@ -239,9 +244,10 @@ internal sealed class SettingsService : ISettingsService
|
||||
|
||||
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();
|
||||
_componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, dict);
|
||||
_componentMessageStore.SaveSection(EnsureKey(subjectId), placementId, effectiveSection, dict);
|
||||
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys ?? [key]));
|
||||
return;
|
||||
}
|
||||
@@ -271,14 +277,14 @@ internal sealed class SettingsService : ISettingsService
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void SaveComponentSnapshot<T>(string? componentId, string? placementId, T 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()
|
||||
|
||||
@@ -9,10 +9,11 @@ using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
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 =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -58,8 +59,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
private string _componentId = BuiltInComponentIds.DesktopClock;
|
||||
private string _placementId = string.Empty;
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
@@ -124,12 +124,6 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
InitializeDialIfNeeded();
|
||||
@@ -376,8 +370,11 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
|
||||
private void LoadClockSettings()
|
||||
{
|
||||
var appSnapshot = _appSettingsService.Load();
|
||||
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
|
||||
var appSnapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
var componentSnapshot = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
|
||||
SettingsScope.ComponentInstance,
|
||||
_componentId,
|
||||
_placementId);
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
|
||||
|
||||
var configuredTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId)
|
||||
|
||||
@@ -10,10 +10,11 @@ using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
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(
|
||||
string Name,
|
||||
@@ -26,8 +27,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
Interval = TimeSpan.FromMinutes(4)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
|
||||
|
||||
@@ -125,16 +125,13 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
private void RefreshSchedule()
|
||||
{
|
||||
var appSettings = _appSettingsService.Load();
|
||||
var componentSettings = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
|
||||
var appSettings = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
var componentSettings = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
|
||||
SettingsScope.ComponentInstance,
|
||||
_componentId,
|
||||
_placementId);
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
UpdateHeader(now);
|
||||
|
||||
@@ -16,10 +16,11 @@ using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
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 =
|
||||
new Dictionary<DayOfWeek, string>
|
||||
@@ -58,8 +59,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
Interval = TimeSpan.FromHours(6)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
|
||||
@@ -149,12 +149,6 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = true;
|
||||
@@ -651,7 +645,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = _settingsService.Load();
|
||||
var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||
}
|
||||
catch
|
||||
@@ -664,7 +658,10 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
|
||||
var snapshot = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
|
||||
SettingsScope.ComponentInstance,
|
||||
_componentId,
|
||||
_placementId);
|
||||
return DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -3,7 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
@@ -14,7 +16,8 @@ public sealed record DesktopComponentControlFactoryContext(
|
||||
IWeatherInfoService WeatherInfoService,
|
||||
IRecommendationInfoService RecommendationInfoService,
|
||||
ICalculatorDataService CalculatorDataService,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore,
|
||||
ISettingsService SettingsService,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
string? PlacementId = null);
|
||||
|
||||
public sealed class DesktopComponentRuntimeRegistration
|
||||
@@ -84,9 +87,10 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
IWeatherInfoService weatherInfoService,
|
||||
IRecommendationInfoService recommendationInfoService,
|
||||
ICalculatorDataService calculatorDataService,
|
||||
IComponentInstanceSettingsStore componentSettingsStore,
|
||||
string? placementId = null)
|
||||
{
|
||||
var settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId);
|
||||
var control = _controlFactory(new DesktopComponentControlFactoryContext(
|
||||
Definition,
|
||||
cellSize,
|
||||
@@ -94,12 +98,14 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
weatherInfoService,
|
||||
recommendationInfoService,
|
||||
calculatorDataService,
|
||||
componentSettingsStore,
|
||||
settingsService,
|
||||
componentAccessor,
|
||||
placementId));
|
||||
var runtimeContext = new DesktopComponentRuntimeContext(
|
||||
Definition.Id,
|
||||
placementId,
|
||||
componentSettingsStore);
|
||||
settingsService,
|
||||
componentAccessor);
|
||||
|
||||
if (control is IComponentRuntimeContextAware runtimeContextAwareComponent)
|
||||
{
|
||||
@@ -111,13 +117,6 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
placementAwareComponent.SetComponentPlacementContext(Definition.Id, placementId);
|
||||
}
|
||||
|
||||
if (control is IComponentSettingsStoreAware settingsStoreAwareComponent)
|
||||
{
|
||||
settingsStoreAwareComponent.SetComponentSettingsStore(componentSettingsStore);
|
||||
}
|
||||
|
||||
ComponentSettingsService.ApplyScopedContextToTarget(control, Definition.Id, placementId);
|
||||
|
||||
if (control is IDesktopComponentWidget sizedComponent)
|
||||
{
|
||||
sizedComponent.ApplyCellSize(cellSize);
|
||||
|
||||
@@ -12,11 +12,12 @@ using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Theme;
|
||||
|
||||
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 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 ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
|
||||
private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;
|
||||
@@ -185,12 +185,6 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||
{
|
||||
_ = isEditMode;
|
||||
@@ -265,7 +259,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
}
|
||||
|
||||
_isRefreshing = true;
|
||||
var app = _settingsService.Load();
|
||||
var app = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(app.LanguageCode);
|
||||
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;
|
||||
@@ -953,7 +947,10 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
|
||||
try
|
||||
{
|
||||
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
|
||||
var snapshot = _settingsService.LoadSnapshot<ComponentSettingsSnapshot>(
|
||||
SettingsScope.ComponentInstance,
|
||||
_componentId,
|
||||
_placementId);
|
||||
enabled = snapshot.WeatherAutoRefreshEnabled;
|
||||
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ using LanMountainDesktop.Theme;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -236,12 +236,6 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||
{
|
||||
_ = isEditMode;
|
||||
|
||||
@@ -16,7 +16,7 @@ using LanMountainDesktop.Theme;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -234,12 +234,6 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||
{
|
||||
_ = isEditMode;
|
||||
|
||||
@@ -16,7 +16,7 @@ using LanMountainDesktop.Services;
|
||||
|
||||
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(
|
||||
string LanguageCode,
|
||||
@@ -128,12 +128,6 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
|
||||
@@ -18,7 +18,7 @@ using LanMountainDesktop.Theme;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -179,12 +179,6 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||
{
|
||||
_ = isEditMode;
|
||||
|
||||
@@ -13,7 +13,7 @@ using LanMountainDesktop.Services;
|
||||
|
||||
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 BaseHeightCells = 2;
|
||||
@@ -159,12 +159,6 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
|
||||
{
|
||||
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
|
||||
@@ -101,19 +101,12 @@ public partial class MainWindow
|
||||
|
||||
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// "Desktop edit" toggle. While editing, show the component library window.
|
||||
if (_isComponentLibraryOpen)
|
||||
{
|
||||
CloseComponentLibraryWindow(reopenSettings: false);
|
||||
return;
|
||||
}
|
||||
|
||||
OpenComponentLibraryWindow();
|
||||
_componentLibraryWindowService.Toggle(this);
|
||||
}
|
||||
|
||||
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
CloseComponentLibraryWindow(reopenSettings: false);
|
||||
_componentLibraryWindowService.Close(this);
|
||||
}
|
||||
|
||||
private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e)
|
||||
@@ -202,21 +195,6 @@ public partial class MainWindow
|
||||
{
|
||||
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()
|
||||
@@ -233,15 +211,6 @@ public partial class MainWindow
|
||||
Grid.SetColumnSpan(ClockWidget, columnSpan);
|
||||
}
|
||||
}
|
||||
|
||||
if (WallpaperPreviewClockWidget is not null)
|
||||
{
|
||||
WallpaperPreviewClockWidget.IsVisible = showClock;
|
||||
if (showClock)
|
||||
{
|
||||
WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TaskbarContext GetCurrentTaskbarContext()
|
||||
@@ -251,11 +220,7 @@ public partial class MainWindow
|
||||
|
||||
private void ApplyTaskbarActionVisibility(TaskbarContext context)
|
||||
{
|
||||
if (BackToWindowsButton is null ||
|
||||
OpenComponentLibraryButton is null ||
|
||||
WallpaperPreviewBackButtonVisual is null ||
|
||||
WallpaperPreviewComponentLibraryVisual is null ||
|
||||
WallpaperPreviewSettingsButtonIcon is null)
|
||||
if (BackToWindowsButton is null || OpenComponentLibraryButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -266,9 +231,6 @@ public partial class MainWindow
|
||||
|
||||
BackToWindowsButton.IsVisible = showMinimize;
|
||||
OpenComponentLibraryButton.IsVisible = showDesktopEdit;
|
||||
WallpaperPreviewBackButtonVisual.IsVisible = showMinimize;
|
||||
WallpaperPreviewComponentLibraryVisual.IsVisible = showDesktopEdit;
|
||||
WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings;
|
||||
|
||||
if (TaskbarFixedActionsHost is not null)
|
||||
{
|
||||
@@ -280,16 +242,6 @@ public partial class MainWindow
|
||||
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)
|
||||
.Where(action => action.IsVisible)
|
||||
.ToList();
|
||||
@@ -300,11 +252,6 @@ public partial class MainWindow
|
||||
{
|
||||
TaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
|
||||
}
|
||||
|
||||
if (WallpaperPreviewTaskbarDynamicActionsHost is not null)
|
||||
{
|
||||
WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOpenSettingsActionVisualState()
|
||||
@@ -455,14 +402,7 @@ public partial class MainWindow
|
||||
TaskbarDynamicActionsPanel.Children.Clear();
|
||||
}
|
||||
|
||||
if (WallpaperPreviewTaskbarDynamicActionsHost is not null)
|
||||
{
|
||||
WallpaperPreviewTaskbarDynamicActionsHost.Children.Clear();
|
||||
}
|
||||
|
||||
if (actions.Count == 0 ||
|
||||
TaskbarDynamicActionsPanel is null ||
|
||||
WallpaperPreviewTaskbarDynamicActionsHost is null)
|
||||
if (actions.Count == 0 || TaskbarDynamicActionsPanel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -556,37 +496,6 @@ public partial class MainWindow
|
||||
|
||||
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);
|
||||
_componentSettingsService.DeleteForComponent(placement.ComponentId, placement.PlacementId);
|
||||
_componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId);
|
||||
|
||||
ClearDesktopComponentSelection();
|
||||
|
||||
@@ -698,7 +607,7 @@ public partial class MainWindow
|
||||
foreach (var placement in placementsToRemove)
|
||||
{
|
||||
_desktopComponentPlacements.Remove(placement);
|
||||
_componentSettingsService.DeleteForComponent(placement.ComponentId, placement.PlacementId);
|
||||
_componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId);
|
||||
}
|
||||
|
||||
_desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount);
|
||||
@@ -1345,14 +1254,19 @@ public partial class MainWindow
|
||||
{
|
||||
try
|
||||
{
|
||||
var component = runtimeDescriptor.CreateControl(
|
||||
var createContext = new ComponentLibraryCreateContext(
|
||||
cellSize,
|
||||
_timeZoneService,
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_componentSettingsService,
|
||||
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);
|
||||
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()
|
||||
{
|
||||
// Animate component library panel collapsing downward
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@ public partial class MainWindow
|
||||
{
|
||||
private void UpdateCurrentRenderBackendStatus()
|
||||
{
|
||||
if (CurrentRenderBackendLabelTextBlock is null ||
|
||||
CurrentRenderBackendValueTextBlock is null ||
|
||||
CurrentRenderBackendImplementationTextBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var backendInfo = AppRenderBackendDiagnostics.Detect();
|
||||
var localizedBackend = GetLocalizedRenderBackendName(backendInfo.ActualBackend);
|
||||
|
||||
|
||||
@@ -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
404
LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
Normal file
404
LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
Normal 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]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:comp="using:LanMountainDesktop.Views.Components"
|
||||
xmlns:pages="using:LanMountainDesktop.Views.SettingsPages"
|
||||
xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -321,183 +320,6 @@
|
||||
</Border>
|
||||
</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"
|
||||
IsVisible="False"
|
||||
Opacity="0"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -20,7 +20,9 @@ using Avalonia.Threading;
|
||||
using FluentAvalonia.Styling;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Theme;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using LibVLCSharp.Shared;
|
||||
@@ -73,21 +75,26 @@ public partial class MainWindow : Window
|
||||
[
|
||||
TaskbarActionId.MinimizeToWindows
|
||||
];
|
||||
private readonly DesktopGridLayoutService _gridLayoutService = new();
|
||||
private readonly MonetColorService _monetColorService = new();
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly DesktopLayoutSettingsService _desktopLayoutSettingsService = new();
|
||||
private readonly LauncherSettingsService _launcherSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
private readonly IGridSettingsService _gridSettingsService;
|
||||
private readonly IThemeAppearanceService _themeSettingsService;
|
||||
private readonly IWeatherSettingsService _weatherSettingsService;
|
||||
private readonly IRegionSettingsService _regionSettingsService;
|
||||
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 TimeZoneService _timeZoneService = new();
|
||||
private readonly TimeZoneService _timeZoneService;
|
||||
private readonly WindowsStartupService _windowsStartupService = new();
|
||||
private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
|
||||
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
|
||||
private readonly IWeatherInfoService _weatherDataService;
|
||||
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
|
||||
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
|
||||
private readonly ComponentRegistry _componentRegistry;
|
||||
private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry;
|
||||
private readonly IComponentLibraryService _componentLibraryService;
|
||||
private readonly IComponentLibraryWindowService _componentLibraryWindowService = new ComponentLibraryWindowService();
|
||||
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
|
||||
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<TaskbarActionId> _pinnedTaskbarActions = [];
|
||||
@@ -171,71 +178,25 @@ public partial class MainWindow : Window
|
||||
{
|
||||
var pluginRuntimeService = (Application.Current as App)?.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();
|
||||
InitializePluginSettingsNavigation();
|
||||
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
|
||||
_componentRegistry,
|
||||
pluginRuntimeService);
|
||||
_componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry);
|
||||
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
|
||||
AppSettingsService.SettingsSaved += OnExternalAppSettingsSaved;
|
||||
LauncherSettingsService.SettingsSaved += OnExternalLauncherSettingsSaved;
|
||||
PendingRestartStateService.StateChanged += OnPendingRestartStateChanged;
|
||||
_settingsService.Changed += OnSettingsChanged;
|
||||
PropertyChanged += OnWindowPropertyChanged;
|
||||
InitializeDesktopSurfaceSwipeHandlers();
|
||||
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)
|
||||
@@ -275,9 +236,9 @@ public partial class MainWindow : Window
|
||||
base.OnOpened(e);
|
||||
|
||||
_suppressSettingsPersistence = true;
|
||||
var snapshot = _appSettingsService.Load();
|
||||
var desktopLayoutSnapshot = _desktopLayoutSettingsService.Load();
|
||||
var launcherSnapshot = _launcherSettingsService.Load();
|
||||
var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
var desktopLayoutSnapshot = _componentLayoutStore.LoadLayout();
|
||||
var launcherSnapshot = _settingsService.LoadSnapshot<LauncherSettingsSnapshot>(SettingsScope.Launcher);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.TimeZoneId))
|
||||
{
|
||||
@@ -289,47 +250,16 @@ public partial class MainWindow : Window
|
||||
MinShortSideCells,
|
||||
MaxShortSideCells);
|
||||
|
||||
_gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
|
||||
_suppressGridSpacingEvents = true;
|
||||
GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||
_suppressGridSpacingEvents = false;
|
||||
_gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
|
||||
|
||||
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
||||
_suppressGridInsetEvents = true;
|
||||
GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
|
||||
GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
|
||||
_suppressGridInsetEvents = false;
|
||||
GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
|
||||
|
||||
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
|
||||
_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;
|
||||
ApplyTaskbarSettings(snapshot);
|
||||
InitializeLocalization(snapshot.LanguageCode);
|
||||
InitializeWeatherSettings(snapshot);
|
||||
_ = _componentSettingsService.Load();
|
||||
InitializeAutoStartWithWindowsSetting(snapshot);
|
||||
InitializeAppRenderModeSetting(snapshot);
|
||||
InitializeUpdateSettings(snapshot);
|
||||
@@ -349,15 +279,8 @@ public partial class MainWindow : Window
|
||||
|
||||
_isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
|
||||
ApplyNightModeState(_isNightMode, refreshPalettes: true);
|
||||
_suppressStatusBarToggleEvents = true;
|
||||
StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
|
||||
_suppressStatusBarToggleEvents = false;
|
||||
ApplyLocalization();
|
||||
ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor);
|
||||
_settingsContentPanelTransform = SettingsContentPanel.RenderTransform as TranslateTransform;
|
||||
DesktopHost.SizeChanged += OnDesktopHostSizeChanged;
|
||||
WallpaperPreviewHost.SizeChanged += OnWallpaperPreviewHostSizeChanged;
|
||||
GridPreviewHost.SizeChanged += OnGridPreviewHostSizeChanged;
|
||||
RebuildDesktopGrid();
|
||||
LoadLauncherEntriesAsync();
|
||||
InitializeTimeZoneSettings();
|
||||
@@ -384,28 +307,15 @@ public partial class MainWindow : Window
|
||||
_wallpaperPreviewSnapshotBitmap = null;
|
||||
_libVlc?.Dispose();
|
||||
_libVlc = null;
|
||||
if (_weatherDataService is IDisposable weatherServiceDisposable)
|
||||
{
|
||||
weatherServiceDisposable.Dispose();
|
||||
}
|
||||
if (_recommendationInfoService is IDisposable recommendationServiceDisposable)
|
||||
{
|
||||
recommendationServiceDisposable.Dispose();
|
||||
}
|
||||
_releaseUpdateService.Dispose();
|
||||
_wallpaperBitmap?.Dispose();
|
||||
_wallpaperBitmap = null;
|
||||
AppSettingsService.SettingsSaved -= OnExternalAppSettingsSaved;
|
||||
LauncherSettingsService.SettingsSaved -= OnExternalLauncherSettingsSaved;
|
||||
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
|
||||
_settingsService.Changed -= OnSettingsChanged;
|
||||
PropertyChanged -= OnWindowPropertyChanged;
|
||||
DesktopHost.SizeChanged -= OnDesktopHostSizeChanged;
|
||||
WallpaperPreviewHost.SizeChanged -= OnWallpaperPreviewHostSizeChanged;
|
||||
GridPreviewHost.SizeChanged -= OnGridPreviewHostSizeChanged;
|
||||
GridSizeSlider.ValueChanged -= OnGridSizeSliderChanged;
|
||||
GridSizeNumberBox.ValueChanged -= OnGridSizeNumberBoxChanged;
|
||||
GridEdgeInsetNumberBox.ValueChanged -= OnGridEdgeInsetNumberBoxChanged;
|
||||
StatusBarSpacingNumberBox.ValueChanged -= OnStatusBarSpacingNumberBoxChanged;
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
@@ -434,6 +344,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnGridSizeSliderChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (GridSizeSlider is null || GridSizeNumberBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sliderValue = (int)Math.Round(GridSizeSlider.Value);
|
||||
if (Math.Abs(GridSizeNumberBox.Value - sliderValue) > double.Epsilon)
|
||||
{
|
||||
@@ -444,6 +359,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnGridSizeNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
|
||||
{
|
||||
if (GridSizeSlider is null || GridSizeNumberBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var numberBoxValue = (int)Math.Round(GridSizeNumberBox.Value);
|
||||
if (Math.Abs(GridSizeSlider.Value - numberBoxValue) > double.Epsilon)
|
||||
{
|
||||
@@ -454,6 +374,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnGridEdgeInsetSliderChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (GridEdgeInsetSlider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_suppressGridInsetEvents)
|
||||
{
|
||||
return;
|
||||
@@ -466,6 +391,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnGridEdgeInsetNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
|
||||
{
|
||||
if (GridEdgeInsetNumberBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_suppressGridInsetEvents)
|
||||
{
|
||||
return;
|
||||
@@ -511,6 +441,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnStatusBarSpacingModeChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (StatusBarSpacingModeComboBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_suppressStatusBarSpacingEvents)
|
||||
{
|
||||
return;
|
||||
@@ -529,6 +464,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnStatusBarSpacingSliderChanged(object? sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
if (StatusBarSpacingSlider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_suppressStatusBarSpacingEvents)
|
||||
{
|
||||
return;
|
||||
@@ -549,6 +489,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnStatusBarSpacingNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
|
||||
{
|
||||
if (StatusBarSpacingNumberBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_suppressStatusBarSpacingEvents)
|
||||
{
|
||||
return;
|
||||
@@ -597,7 +542,8 @@ public partial class MainWindow : Window
|
||||
GridPreviewHost is null ||
|
||||
GridPreviewViewport is null ||
|
||||
GridPreviewGrid is null ||
|
||||
GridPreviewLinesCanvas is null)
|
||||
GridPreviewLinesCanvas is null ||
|
||||
GridSizeSlider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -626,11 +572,11 @@ public partial class MainWindow : Window
|
||||
|
||||
var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding);
|
||||
var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding);
|
||||
var preset = _gridLayoutService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
|
||||
var gapRatio = _gridLayoutService.ResolveGapRatio(preset);
|
||||
var preset = _gridSettingsService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
|
||||
var gapRatio = _gridSettingsService.ResolveGapRatio(preset);
|
||||
var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
|
||||
var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent);
|
||||
var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset);
|
||||
var edgeInset = _gridSettingsService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent);
|
||||
var gridMetrics = _gridSettingsService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset);
|
||||
if (gridMetrics.CellSize <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -676,12 +622,15 @@ public partial class MainWindow : Window
|
||||
ApplyStatusBarComponentSpacingForPanel(GridPreviewTopStatusComponentsPanel, gridMetrics.CellSize);
|
||||
UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize);
|
||||
|
||||
GridInfoTextBlock.Text = Lf(
|
||||
"settings.grid.info_format",
|
||||
"Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
|
||||
gridMetrics.ColumnCount,
|
||||
gridMetrics.RowCount,
|
||||
gridMetrics.CellSize);
|
||||
if (GridInfoTextBlock is not null)
|
||||
{
|
||||
GridInfoTextBlock.Text = Lf(
|
||||
"settings.grid.info_format",
|
||||
"Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
|
||||
gridMetrics.ColumnCount,
|
||||
gridMetrics.RowCount,
|
||||
gridMetrics.CellSize);
|
||||
}
|
||||
|
||||
DrawGridPreviewLines(gridMetrics);
|
||||
}
|
||||
@@ -767,7 +716,12 @@ public partial class MainWindow : Window
|
||||
|
||||
private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(
|
||||
if (GridSizeNumberBox is null || GridSizeSlider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(
|
||||
TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
|
||||
_desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
|
||||
|
||||
@@ -825,9 +779,9 @@ public partial class MainWindow : Window
|
||||
{
|
||||
var hostWidth = DesktopHost.Bounds.Width;
|
||||
var hostHeight = DesktopHost.Bounds.Height;
|
||||
var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset);
|
||||
var edgeInset = _gridLayoutService.CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
|
||||
var gridMetrics = _gridLayoutService.CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset);
|
||||
var gapRatio = _gridSettingsService.ResolveGapRatio(_gridSpacingPreset);
|
||||
var edgeInset = _gridSettingsService.CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
|
||||
var gridMetrics = _gridSettingsService.CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset);
|
||||
if (gridMetrics.CellSize <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -875,12 +829,15 @@ public partial class MainWindow : Window
|
||||
UpdateDesktopSurfaceLayout(gridMetrics);
|
||||
UpdateSettingsViewportInsets(gridMetrics.CellSize);
|
||||
|
||||
GridInfoTextBlock.Text = Lf(
|
||||
"settings.grid.info_format",
|
||||
"Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
|
||||
gridMetrics.ColumnCount,
|
||||
gridMetrics.RowCount,
|
||||
gridMetrics.CellSize);
|
||||
if (GridInfoTextBlock is not null)
|
||||
{
|
||||
GridInfoTextBlock.Text = Lf(
|
||||
"settings.grid.info_format",
|
||||
"Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
|
||||
gridMetrics.ColumnCount,
|
||||
gridMetrics.RowCount,
|
||||
gridMetrics.CellSize);
|
||||
}
|
||||
|
||||
UpdateWallpaperPreviewLayout();
|
||||
}
|
||||
@@ -930,6 +887,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private int ResolvePendingGridEdgeInsetPercent()
|
||||
{
|
||||
if (GridEdgeInsetNumberBox is null)
|
||||
{
|
||||
return _desktopEdgeInsetPercent;
|
||||
}
|
||||
|
||||
var pending = (int)Math.Round(GridEdgeInsetNumberBox.Value);
|
||||
return Math.Clamp(pending, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
||||
}
|
||||
@@ -1067,47 +1029,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private void UpdateSettingsViewportInsets(double cellSize)
|
||||
{
|
||||
if (SettingsContentPanel is null)
|
||||
{
|
||||
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;
|
||||
_ = cellSize;
|
||||
}
|
||||
|
||||
private void UpdateWallpaperPreviewLayout()
|
||||
@@ -1160,9 +1082,9 @@ public partial class MainWindow : Window
|
||||
|
||||
var innerWidth = Math.Max(1, previewWidth - horizontalPadding);
|
||||
var innerHeight = Math.Max(1, previewHeight - verticalPadding);
|
||||
var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset);
|
||||
var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
|
||||
var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset);
|
||||
var gapRatio = _gridSettingsService.ResolveGapRatio(_gridSpacingPreset);
|
||||
var edgeInset = _gridSettingsService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
|
||||
var gridMetrics = _gridSettingsService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset);
|
||||
if (gridMetrics.CellSize <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -1273,6 +1195,11 @@ public partial class MainWindow : Window
|
||||
|
||||
private void InitializeTimeZoneSettings()
|
||||
{
|
||||
if (TimeZoneComboBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate timezone dropdown items before selecting current timezone.
|
||||
_suppressTimeZoneSelectionEvents = true;
|
||||
TimeZoneComboBox.Items.Clear();
|
||||
@@ -1299,7 +1226,9 @@ public partial class MainWindow : Window
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class AboutSettingsPage : UserControl
|
||||
{
|
||||
public AboutSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class AppearanceSettingsPage : UserControl
|
||||
{
|
||||
public AppearanceSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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="日夜模式"
|
||||
Description="切换应用的浅色或深色主题。">
|
||||
<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="主题色"
|
||||
Description="选择应用的主题点缀色。">
|
||||
<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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class ColorSettingsPage : UserControl
|
||||
{
|
||||
public ColorSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class ComponentsSettingsPage : UserControl
|
||||
{
|
||||
public ComponentsSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class GeneralSettingsPage : UserControl
|
||||
{
|
||||
public GeneralSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class GridSettingsPage : UserControl
|
||||
{
|
||||
public GridSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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="" 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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class LauncherSettingsPage : UserControl
|
||||
{
|
||||
public LauncherSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class RegionSettingsPage : UserControl
|
||||
{
|
||||
public RegionSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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="系统时钟"
|
||||
Description="在状态栏上显示时间。"
|
||||
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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class StatusBarSettingsPage : UserControl
|
||||
{
|
||||
public StatusBarSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class UpdateSettingsPage : UserControl
|
||||
{
|
||||
public UpdateSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class WallpaperSettingsPage : UserControl
|
||||
{
|
||||
public WallpaperSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class WeatherSettingsPage : UserControl
|
||||
{
|
||||
public WeatherSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
{
|
||||
private void ApplyPluginMarketSettingsLocalization()
|
||||
{
|
||||
PluginMarketSettingsPanel.RefreshFromRuntime();
|
||||
}
|
||||
}
|
||||
@@ -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")!;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,12 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
|
||||
private readonly PluginLoaderOptions _loaderOptions;
|
||||
private readonly PluginLoader _loader;
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly IHostApplicationLifecycle _applicationLifecycle = new HostApplicationLifecycleService();
|
||||
private readonly PluginExportRegistry _exportRegistry = new();
|
||||
private readonly PluginSharedContractManager _sharedContractManager;
|
||||
private readonly IServiceProvider _hostServices;
|
||||
private readonly IPluginPackageManager _packageManager;
|
||||
private readonly SettingsFacadeService _settingsFacade;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly SettingsCatalogService _settingsCatalogService;
|
||||
private readonly List<LoadedPlugin> _loadedPlugins = [];
|
||||
private readonly List<PluginLoadResult> _loadResults = [];
|
||||
@@ -39,14 +38,19 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
private readonly List<PluginDesktopComponentContribution> _desktopComponents = [];
|
||||
private readonly object _packageMutationGate = new();
|
||||
|
||||
public PluginRuntimeService()
|
||||
public PluginRuntimeService(ISettingsFacadeService? settingsFacade = null)
|
||||
{
|
||||
PluginsDirectory = Path.Combine(AppContext.BaseDirectory, "Extensions", "Plugins");
|
||||
_sharedContractManager = new PluginSharedContractManager(
|
||||
Path.Combine(GetUserDataRootDirectory(), "PluginMarket"));
|
||||
_packageManager = new PluginRuntimePackageManager(this);
|
||||
_settingsFacade = new SettingsFacadeService(this);
|
||||
_settingsCatalogService = (SettingsCatalogService)_settingsFacade.Catalog;
|
||||
_settingsFacade = settingsFacade ?? new SettingsFacadeService();
|
||||
_settingsCatalogService = _settingsFacade.Catalog as SettingsCatalogService
|
||||
?? new SettingsCatalogService();
|
||||
if (_settingsFacade is SettingsFacadeService concreteFacade)
|
||||
{
|
||||
concreteFacade.BindPluginRuntime(this);
|
||||
}
|
||||
_hostServices = new PluginHostServiceProvider(
|
||||
_packageManager,
|
||||
_applicationLifecycle,
|
||||
@@ -81,7 +85,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
|
||||
|
||||
var disabledPluginIds = GetDisabledPluginIds();
|
||||
var settingsSnapshot = _appSettingsService.Load();
|
||||
var settingsSnapshot = LoadAppSettingsSnapshot();
|
||||
var hostLanguageCode = PluginLocalizer.NormalizeLanguageCode(settingsSnapshot.LanguageCode);
|
||||
var hostProperties = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -222,7 +226,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
var snapshot = _appSettingsService.Load();
|
||||
var snapshot = LoadAppSettingsSnapshot();
|
||||
var disabledPluginIds = snapshot.DisabledPluginIds is { Count: > 0 }
|
||||
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
|
||||
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -239,7 +243,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
snapshot.DisabledPluginIds = disabledPluginIds
|
||||
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
_appSettingsService.Save(snapshot);
|
||||
SaveAppSettingsSnapshot(snapshot);
|
||||
PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true);
|
||||
|
||||
for (var i = 0; i < _catalog.Count; i++)
|
||||
@@ -386,7 +390,10 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
{
|
||||
UnloadInstalledPlugins();
|
||||
_sharedContractManager.Dispose();
|
||||
_settingsFacade.Dispose();
|
||||
if (_settingsFacade is IDisposable disposable && !ReferenceEquals(_settingsFacade, HostSettingsFacadeProvider.GetOrCreate()))
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnloadInstalledPlugins()
|
||||
@@ -409,7 +416,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
|
||||
private HashSet<string> GetDisabledPluginIds()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
var snapshot = LoadAppSettingsSnapshot();
|
||||
return snapshot.DisabledPluginIds is { Count: > 0 }
|
||||
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
|
||||
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -781,13 +788,23 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
|
||||
private void RemovePluginFromSnapshot(string pluginId)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
var snapshot = LoadAppSettingsSnapshot();
|
||||
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)
|
||||
{
|
||||
_catalog.RemoveAll(entry => string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
114
LanMountainDesktop/plugins/PluginScopedSettingsService.cs
Normal file
114
LanMountainDesktop/plugins/PluginScopedSettingsService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -26,8 +26,6 @@
|
||||
- `PluginLoadContext.cs`
|
||||
- `PluginRuntimeService.cs`
|
||||
- `PluginCatalogEntry.cs`
|
||||
- `PluginSettingsPage.axaml`
|
||||
- `PluginSettingsPage.Host.cs`
|
||||
- `PluginMarketIndexService.cs`
|
||||
- `PluginMarketInstallService.cs`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user