Compare commits

...

4 Commits

Author SHA1 Message Date
lincube
cab35f4c22 0.5.1
插件系统试验
2026-03-09 12:27:33 +08:00
lincube
c9f92a4755 0.5.0
设置优化
2026-03-08 14:00:13 +08:00
lincube
854deae801 0.4.12
模块化解耦
2026-03-08 04:22:19 +08:00
lincube
d72cd42483 0.4.11 2026-03-08 03:23:03 +08:00
106 changed files with 12497 additions and 1629 deletions

6
.gitignore vendored
View File

@@ -482,3 +482,9 @@ $RECYCLE.BIN/
*.swp
nul
/publish-test
/_build_verify
/_build_verify_tray
/_build_verify_plugin
/_build_verify_plugin_tabs
/_build_verify_sample_plugin
/_build_verify_sample_plugin_capabilities

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.PluginSdk;
public interface IPlugin
{
void Initialize(IPluginContext context);
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public interface IPluginContext
{
PluginManifest Manifest { get; }
string PluginDirectory { get; }
string DataDirectory { get; }
IServiceProvider Services { get; }
IReadOnlyDictionary<string, object?> Properties { get; }
T? GetService<T>();
bool TryGetProperty<T>(string key, out T? value);
void RegisterSettingsPage(PluginSettingsPageRegistration registration);
void RegisterDesktopComponent(PluginDesktopComponentRegistration registration);
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.12" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,74 @@
using System.Reflection;
using System.Threading;
namespace LanMountainDesktop.PluginSdk;
public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
{
private int _disposed;
internal LoadedPlugin(
PluginManifest manifest,
string sourcePath,
string assemblyPath,
Assembly assembly,
IPlugin plugin,
IPluginContext context,
IReadOnlyList<PluginSettingsPageRegistration> settingsPages,
IReadOnlyList<PluginDesktopComponentRegistration> desktopComponents,
PluginLoadContext loadContext)
{
Manifest = manifest;
SourcePath = sourcePath;
AssemblyPath = assemblyPath;
Assembly = assembly;
Plugin = plugin;
Context = context;
SettingsPages = settingsPages;
DesktopComponents = desktopComponents;
LoadContext = loadContext;
}
public PluginManifest Manifest { get; }
public string SourcePath { get; }
public string AssemblyPath { get; }
public Assembly Assembly { get; }
public IPlugin Plugin { get; }
public IPluginContext Context { get; }
public IReadOnlyList<PluginSettingsPageRegistration> SettingsPages { get; }
public IReadOnlyList<PluginDesktopComponentRegistration> DesktopComponents { get; }
public PluginLoadContext LoadContext { get; }
public void Dispose()
{
DisposeAsync().AsTask().GetAwaiter().GetResult();
}
public async ValueTask DisposeAsync()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
{
return;
}
if (Plugin is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else if (Plugin is IDisposable disposable)
{
disposable.Dispose();
}
LoadContext.Unload();
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,8 @@
namespace LanMountainDesktop.PluginSdk;
public abstract class PluginBase : IPlugin
{
public virtual void Initialize(IPluginContext context)
{
}
}

View File

@@ -0,0 +1,66 @@
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginDesktopComponentContext
{
public PluginDesktopComponentContext(
PluginManifest manifest,
string pluginDirectory,
string dataDirectory,
IServiceProvider services,
IReadOnlyDictionary<string, object?> properties,
string componentId,
string? placementId,
double cellSize)
{
ArgumentNullException.ThrowIfNull(manifest);
ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory);
ArgumentException.ThrowIfNullOrWhiteSpace(dataDirectory);
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(properties);
Manifest = manifest;
PluginDirectory = pluginDirectory;
DataDirectory = dataDirectory;
Services = services;
Properties = properties;
ComponentId = componentId.Trim();
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
CellSize = Math.Max(1, cellSize);
}
public PluginManifest Manifest { get; }
public string PluginDirectory { get; }
public string DataDirectory { get; }
public IServiceProvider Services { get; }
public IReadOnlyDictionary<string, object?> Properties { get; }
public string ComponentId { get; }
public string? PlacementId { get; }
public double CellSize { get; }
public T? GetService<T>()
{
return (T?)Services.GetService(typeof(T));
}
public bool TryGetProperty<T>(string key, out T? value)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);
if (Properties.TryGetValue(key, out var rawValue) && rawValue is T typedValue)
{
value = typedValue;
return true;
}
value = default;
return false;
}
}

View File

@@ -0,0 +1,66 @@
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginDesktopComponentRegistration
{
public PluginDesktopComponentRegistration(
string componentId,
string displayName,
Func<PluginDesktopComponentContext, Control> controlFactory,
string iconKey = "PuzzlePiece",
string category = "Plugins",
int minWidthCells = 2,
int minHeightCells = 2,
bool allowDesktopPlacement = true,
bool allowStatusBarPlacement = false,
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
string? displayNameLocalizationKey = null,
Func<double, double>? cornerRadiusResolver = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
ArgumentException.ThrowIfNullOrWhiteSpace(displayName);
ArgumentException.ThrowIfNullOrWhiteSpace(iconKey);
ArgumentException.ThrowIfNullOrWhiteSpace(category);
ArgumentNullException.ThrowIfNull(controlFactory);
ComponentId = componentId.Trim();
DisplayName = displayName.Trim();
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(displayNameLocalizationKey)
? null
: displayNameLocalizationKey.Trim();
ControlFactory = controlFactory;
IconKey = iconKey.Trim();
Category = category.Trim();
MinWidthCells = Math.Max(1, minWidthCells);
MinHeightCells = Math.Max(1, minHeightCells);
AllowDesktopPlacement = allowDesktopPlacement;
AllowStatusBarPlacement = allowStatusBarPlacement;
ResizeMode = resizeMode;
CornerRadiusResolver = cornerRadiusResolver;
}
public string ComponentId { get; }
public string DisplayName { get; }
public string? DisplayNameLocalizationKey { get; }
public Func<PluginDesktopComponentContext, Control> ControlFactory { get; }
public string IconKey { get; }
public string Category { get; }
public int MinWidthCells { get; }
public int MinHeightCells { get; }
public bool AllowDesktopPlacement { get; }
public bool AllowStatusBarPlacement { get; }
public PluginDesktopComponentResizeMode ResizeMode { get; }
public Func<double, double>? CornerRadiusResolver { get; }
}

View File

@@ -0,0 +1,7 @@
namespace LanMountainDesktop.PluginSdk;
public enum PluginDesktopComponentResizeMode
{
Proportional = 0,
Free = 1
}

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.PluginSdk;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class PluginEntranceAttribute : Attribute
{
}

View File

@@ -0,0 +1,66 @@
using System.Reflection;
using System.Runtime.Loader;
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
private readonly HashSet<string> _sharedAssemblyNames;
public PluginLoadContext(string mainAssemblyPath, IEnumerable<string>? sharedAssemblyNames = null)
: base($"{Path.GetFileNameWithoutExtension(mainAssemblyPath)}_{Guid.NewGuid():N}", isCollectible: true)
{
ArgumentException.ThrowIfNullOrWhiteSpace(mainAssemblyPath);
MainAssemblyPath = Path.GetFullPath(mainAssemblyPath);
_resolver = new AssemblyDependencyResolver(MainAssemblyPath);
_sharedAssemblyNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
typeof(IPlugin).Assembly.GetName().Name!
};
if (sharedAssemblyNames is null)
{
return;
}
foreach (var assemblyName in sharedAssemblyNames)
{
if (!string.IsNullOrWhiteSpace(assemblyName))
{
_sharedAssemblyNames.Add(assemblyName.Trim());
}
}
}
public string MainAssemblyPath { get; }
protected override Assembly? Load(AssemblyName assemblyName)
{
var simpleName = assemblyName.Name;
if (string.IsNullOrWhiteSpace(simpleName))
{
return null;
}
if (_sharedAssemblyNames.Contains(simpleName))
{
return Default.Assemblies.FirstOrDefault(
assembly => string.Equals(
assembly.GetName().Name,
simpleName,
StringComparison.OrdinalIgnoreCase))
?? null;
}
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
return assemblyPath is null ? null : LoadFromAssemblyPath(assemblyPath);
}
protected override nint LoadUnmanagedDll(string unmanagedDllName)
{
var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
return libraryPath is null ? nint.Zero : LoadUnmanagedDllFromPath(libraryPath);
}
}

View File

@@ -0,0 +1,20 @@
namespace LanMountainDesktop.PluginSdk;
public sealed record PluginLoadResult(
string SourcePath,
PluginManifest? Manifest,
LoadedPlugin? LoadedPlugin,
Exception? Error)
{
public bool IsSuccess => LoadedPlugin is not null && Error is null;
public static PluginLoadResult Success(string sourcePath, PluginManifest manifest, LoadedPlugin loadedPlugin)
{
return new PluginLoadResult(sourcePath, manifest, loadedPlugin, null);
}
public static PluginLoadResult Failure(string sourcePath, PluginManifest? manifest, Exception error)
{
return new PluginLoadResult(sourcePath, manifest, null, error);
}
}

View File

@@ -0,0 +1,616 @@
using System.Collections.ObjectModel;
using System.IO.Compression;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginLoader
{
private readonly PluginLoaderOptions _options;
public PluginLoader(PluginLoaderOptions? options = null)
{
_options = options ?? new PluginLoaderOptions();
}
public IReadOnlyList<PluginLoadResult> LoadAll(
string pluginsRootDirectory,
IServiceProvider? services = null,
IReadOnlyDictionary<string, object?>? properties = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pluginsRootDirectory);
if (!Directory.Exists(pluginsRootDirectory))
{
return Array.Empty<PluginLoadResult>();
}
var results = new List<PluginLoadResult>();
var candidates = DiscoverCandidates(pluginsRootDirectory, results);
var selectedPluginIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var candidate in candidates)
{
if (!selectedPluginIds.Add(candidate.Manifest.Id))
{
results.Add(PluginLoadResult.Failure(
candidate.SourcePath,
candidate.Manifest,
new InvalidOperationException(
$"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected.")));
continue;
}
results.Add(candidate.SourceKind switch
{
PluginSourceKind.Package => LoadFromPackage(
candidate.SourcePath,
pluginsRootDirectory,
candidate.Manifest,
services,
properties),
_ => LoadFromManifest(
candidate.SourcePath,
candidate.Manifest,
services,
properties)
});
}
return results;
}
public PluginLoadResult LoadFromManifest(
string manifestPath,
IServiceProvider? services = null,
IReadOnlyDictionary<string, object?>? properties = null)
{
PluginManifest? manifest = null;
try
{
manifest = PluginManifest.Load(manifestPath);
return LoadFromManifest(manifestPath, manifest, services, properties);
}
catch (Exception ex)
{
return PluginLoadResult.Failure(Path.GetFullPath(manifestPath), manifest, ex);
}
}
public PluginLoadResult LoadFromPackage(
string packagePath,
string pluginsRootDirectory,
IServiceProvider? services = null,
IReadOnlyDictionary<string, object?>? properties = null)
{
PluginManifest? manifest = null;
try
{
manifest = ReadManifestFromPackage(packagePath);
return LoadFromPackage(packagePath, pluginsRootDirectory, manifest, services, properties);
}
catch (Exception ex)
{
return PluginLoadResult.Failure(Path.GetFullPath(packagePath), manifest, ex);
}
}
public PluginLoadResult LoadFromAssembly(
string assemblyPath,
PluginManifest manifest,
IServiceProvider? services = null,
IReadOnlyDictionary<string, object?>? properties = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(assemblyPath);
ArgumentNullException.ThrowIfNull(manifest);
var fullAssemblyPath = Path.GetFullPath(assemblyPath);
var pluginDirectory = Path.GetDirectoryName(fullAssemblyPath)
?? throw new InvalidOperationException($"Failed to determine the plugin directory of '{fullAssemblyPath}'.");
var dataDirectory = Path.Combine(pluginDirectory, _options.DataDirectoryName);
return LoadCore(fullAssemblyPath, fullAssemblyPath, pluginDirectory, dataDirectory, manifest, services, properties);
}
private PluginLoadResult LoadCore(
string sourcePath,
string assemblyPath,
string pluginDirectory,
string dataDirectory,
PluginManifest manifest,
IServiceProvider? services,
IReadOnlyDictionary<string, object?>? properties)
{
PluginLoadContext? loadContext = null;
try
{
loadContext = new PluginLoadContext(assemblyPath, _options.SharedAssemblyNames);
var assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
var pluginType = ResolvePluginType(assembly);
var plugin = CreatePluginInstance(pluginType);
var context = CreateContext(manifest, pluginDirectory, dataDirectory, services, properties);
plugin.Initialize(context);
var settingsPages = context.GetSettingsPagesSnapshot();
var desktopComponents = context.GetDesktopComponentsSnapshot();
var loadedPlugin = new LoadedPlugin(
manifest,
sourcePath,
assemblyPath,
assembly,
plugin,
context,
settingsPages,
desktopComponents,
loadContext);
return PluginLoadResult.Success(sourcePath, manifest, loadedPlugin);
}
catch (Exception ex)
{
loadContext?.Unload();
return PluginLoadResult.Failure(sourcePath, manifest, ex);
}
}
private PluginLoadResult LoadFromManifest(
string manifestPath,
PluginManifest manifest,
IServiceProvider? services,
IReadOnlyDictionary<string, object?>? properties)
{
try
{
var fullManifestPath = Path.GetFullPath(manifestPath);
var assemblyPath = manifest.ResolveEntranceAssemblyPath(fullManifestPath);
if (!File.Exists(assemblyPath))
{
throw new FileNotFoundException(
$"Plugin '{manifest.Id}' entrance assembly '{assemblyPath}' was not found.",
assemblyPath);
}
var pluginDirectory = Path.GetDirectoryName(assemblyPath)
?? throw new InvalidOperationException($"Failed to determine the plugin directory of '{assemblyPath}'.");
var dataDirectory = Path.Combine(pluginDirectory, _options.DataDirectoryName);
return LoadCore(fullManifestPath, assemblyPath, pluginDirectory, dataDirectory, manifest, services, properties);
}
catch (Exception ex)
{
return PluginLoadResult.Failure(Path.GetFullPath(manifestPath), manifest, ex);
}
}
private PluginLoadResult LoadFromPackage(
string packagePath,
string pluginsRootDirectory,
PluginManifest manifest,
IServiceProvider? services,
IReadOnlyDictionary<string, object?>? properties)
{
try
{
var fullPackagePath = Path.GetFullPath(packagePath);
var extractionDirectory = ExtractPackage(fullPackagePath, pluginsRootDirectory);
var extractedManifestPath = Path.Combine(extractionDirectory, _options.ManifestFileName);
if (!File.Exists(extractedManifestPath))
{
throw new FileNotFoundException(
$"Plugin package '{fullPackagePath}' does not contain '{_options.ManifestFileName}'.",
extractedManifestPath);
}
var extractedManifest = PluginManifest.Load(extractedManifestPath);
if (!string.Equals(extractedManifest.Id, manifest.Id, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Plugin package '{fullPackagePath}' manifest id changed after extraction. Expected '{manifest.Id}', actual '{extractedManifest.Id}'.");
}
var assemblyPath = extractedManifest.ResolveEntranceAssemblyPath(extractedManifestPath);
if (!File.Exists(assemblyPath))
{
throw new FileNotFoundException(
$"Plugin '{extractedManifest.Id}' entrance assembly '{assemblyPath}' was not found after package extraction.",
assemblyPath);
}
var dataDirectory = GetPackagedDataDirectory(pluginsRootDirectory, extractedManifest);
return LoadCore(fullPackagePath, assemblyPath, extractionDirectory, dataDirectory, extractedManifest, services, properties);
}
catch (Exception ex)
{
return PluginLoadResult.Failure(Path.GetFullPath(packagePath), manifest, ex);
}
}
private PluginContext CreateContext(
PluginManifest manifest,
string pluginDirectory,
string dataDirectory,
IServiceProvider? services,
IReadOnlyDictionary<string, object?>? properties)
{
Directory.CreateDirectory(dataDirectory);
return new PluginContext(
manifest,
pluginDirectory,
dataDirectory,
services ?? NullServiceProvider.Instance,
CreateReadOnlyProperties(properties));
}
private IReadOnlyList<PluginCandidate> DiscoverCandidates(
string pluginsRootDirectory,
List<PluginLoadResult> preparationFailures)
{
var candidates = new List<PluginCandidate>();
foreach (var packagePath in EnumerateCandidatePaths(
pluginsRootDirectory,
"*" + NormalizePackageExtension(_options.PackageFileExtension)))
{
try
{
var manifest = ReadManifestFromPackage(packagePath);
candidates.Add(new PluginCandidate(Path.GetFullPath(packagePath), manifest, PluginSourceKind.Package));
}
catch (Exception ex)
{
preparationFailures.Add(PluginLoadResult.Failure(Path.GetFullPath(packagePath), null, ex));
}
}
foreach (var manifestPath in EnumerateCandidatePaths(pluginsRootDirectory, _options.ManifestFileName))
{
try
{
var manifest = PluginManifest.Load(manifestPath);
candidates.Add(new PluginCandidate(Path.GetFullPath(manifestPath), manifest, PluginSourceKind.Manifest));
}
catch (Exception ex)
{
preparationFailures.Add(PluginLoadResult.Failure(Path.GetFullPath(manifestPath), null, ex));
}
}
return candidates
.OrderBy(candidate => candidate.SourceKind)
.ThenBy(candidate => candidate.SourcePath, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private IEnumerable<string> EnumerateCandidatePaths(string pluginsRootDirectory, string searchPattern)
{
var runtimeRootDirectory = EnsureTrailingSeparator(GetRuntimeRootDirectory(pluginsRootDirectory));
return Directory
.EnumerateFiles(pluginsRootDirectory, searchPattern, SearchOption.AllDirectories)
.Select(Path.GetFullPath)
.Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase))
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
}
private PluginManifest ReadManifestFromPackage(string packagePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packagePath);
var fullPackagePath = Path.GetFullPath(packagePath);
if (!File.Exists(fullPackagePath))
{
throw new FileNotFoundException($"Plugin package '{fullPackagePath}' was not found.", fullPackagePath);
}
using var archive = ZipFile.OpenRead(fullPackagePath);
var manifestEntries = archive.Entries
.Where(entry =>
!string.IsNullOrWhiteSpace(entry.Name) &&
string.Equals(entry.Name, _options.ManifestFileName, StringComparison.OrdinalIgnoreCase))
.ToArray();
if (manifestEntries.Length == 0)
{
throw new InvalidOperationException(
$"Plugin package '{fullPackagePath}' does not contain '{_options.ManifestFileName}'.");
}
if (manifestEntries.Length > 1)
{
throw new InvalidOperationException(
$"Plugin package '{fullPackagePath}' contains multiple '{_options.ManifestFileName}' files.");
}
using var stream = manifestEntries[0].Open();
return PluginManifest.Load(stream, $"{fullPackagePath}!/{manifestEntries[0].FullName}");
}
private string ExtractPackage(string packagePath, string pluginsRootDirectory)
{
var extractionDirectory = GetPackageExtractionDirectory(pluginsRootDirectory, packagePath);
RecreateDirectory(extractionDirectory);
ZipFile.ExtractToDirectory(packagePath, extractionDirectory, overwriteFiles: true);
return extractionDirectory;
}
private string GetPackageExtractionDirectory(string pluginsRootDirectory, string packagePath)
{
var packageName = SanitizeDirectoryName(Path.GetFileNameWithoutExtension(packagePath));
var packageHash = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(Path.GetFullPath(packagePath))))
.Substring(0, 12);
return Path.Combine(
GetRuntimeRootDirectory(pluginsRootDirectory),
_options.ExtractedPackagesDirectoryName,
$"{packageName}_{packageHash}");
}
private string GetPackagedDataDirectory(string pluginsRootDirectory, PluginManifest manifest)
{
return Path.Combine(
GetRuntimeRootDirectory(pluginsRootDirectory),
_options.PackagedDataDirectoryName,
SanitizeDirectoryName(manifest.Id));
}
private string GetRuntimeRootDirectory(string pluginsRootDirectory)
{
return Path.Combine(Path.GetFullPath(pluginsRootDirectory), _options.RuntimeDirectoryName);
}
private static void RecreateDirectory(string directoryPath)
{
if (Directory.Exists(directoryPath))
{
Directory.Delete(directoryPath, recursive: true);
}
Directory.CreateDirectory(directoryPath);
}
private static string NormalizePackageExtension(string extension)
{
ArgumentException.ThrowIfNullOrWhiteSpace(extension);
return extension.StartsWith(".", StringComparison.Ordinal) ? extension : "." + extension;
}
private static string EnsureTrailingSeparator(string path)
{
var fullPath = Path.GetFullPath(path);
return fullPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
? fullPath
: fullPath + Path.DirectorySeparatorChar;
}
private static string SanitizeDirectoryName(string value)
{
var invalidCharacters = Path.GetInvalidFileNameChars();
var builder = new StringBuilder(value.Length);
foreach (var ch in value)
{
builder.Append(invalidCharacters.Contains(ch) ? '_' : ch);
}
return string.IsNullOrWhiteSpace(builder.ToString()) ? "_plugin" : builder.ToString().Trim();
}
private static ReadOnlyDictionary<string, object?> CreateReadOnlyProperties(
IReadOnlyDictionary<string, object?>? properties)
{
if (properties is null || properties.Count == 0)
{
return new ReadOnlyDictionary<string, object?>(
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase));
}
var map = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in properties)
{
map[pair.Key] = pair.Value;
}
return new ReadOnlyDictionary<string, object?>(map);
}
private static Type ResolvePluginType(Assembly assembly)
{
var candidateTypes = GetLoadableTypes(assembly)
.Where(type =>
typeof(IPlugin).IsAssignableFrom(type) &&
!type.IsAbstract &&
!type.IsInterface &&
!type.ContainsGenericParameters)
.ToArray();
if (candidateTypes.Length == 0)
{
throw new InvalidOperationException(
$"Assembly '{assembly.Location}' does not contain a concrete type implementing '{nameof(IPlugin)}'.");
}
var attributedTypes = candidateTypes
.Where(type => type.IsDefined(typeof(PluginEntranceAttribute), inherit: false))
.ToArray();
if (attributedTypes.Length == 1)
{
return attributedTypes[0];
}
if (attributedTypes.Length > 1)
{
throw new InvalidOperationException(
$"Assembly '{assembly.Location}' contains multiple plugin entrance types. Mark only one type with '{nameof(PluginEntranceAttribute)}'.");
}
if (candidateTypes.Length == 1)
{
return candidateTypes[0];
}
throw new InvalidOperationException(
$"Assembly '{assembly.Location}' contains multiple '{nameof(IPlugin)}' implementations. Mark the intended entrance type with '{nameof(PluginEntranceAttribute)}'.");
}
private static IPlugin CreatePluginInstance(Type pluginType)
{
if (pluginType.GetConstructor(Type.EmptyTypes) is null)
{
throw new InvalidOperationException(
$"Plugin type '{pluginType.FullName}' must expose a public parameterless constructor.");
}
if (Activator.CreateInstance(pluginType) is not IPlugin plugin)
{
throw new InvalidOperationException(
$"Failed to create plugin instance of type '{pluginType.FullName}'.");
}
return plugin;
}
private static Type[] GetLoadableTypes(Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
var loaderMessages = ex.LoaderExceptions
.Where(exception => exception is not null)
.Select(exception => exception!.Message)
.ToArray();
var detail = loaderMessages.Length == 0
? "No additional loader diagnostics were provided."
: string.Join(Environment.NewLine, loaderMessages);
throw new InvalidOperationException(
$"Failed to inspect plugin assembly '{assembly.Location}'.{Environment.NewLine}{detail}",
ex);
}
}
private sealed class PluginContext : IPluginContext
{
private readonly Dictionary<string, PluginSettingsPageRegistration> _settingsPages =
new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, PluginDesktopComponentRegistration> _desktopComponents =
new(StringComparer.OrdinalIgnoreCase);
public PluginContext(
PluginManifest manifest,
string pluginDirectory,
string dataDirectory,
IServiceProvider services,
IReadOnlyDictionary<string, object?> properties)
{
Manifest = manifest;
PluginDirectory = pluginDirectory;
DataDirectory = dataDirectory;
Services = services;
Properties = properties;
}
public PluginManifest Manifest { get; }
public string PluginDirectory { get; }
public string DataDirectory { get; }
public IServiceProvider Services { get; }
public IReadOnlyDictionary<string, object?> Properties { get; }
public T? GetService<T>()
{
return (T?)Services.GetService(typeof(T));
}
public bool TryGetProperty<T>(string key, out T? value)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);
if (Properties.TryGetValue(key, out var rawValue) && rawValue is T typedValue)
{
value = typedValue;
return true;
}
value = default;
return false;
}
public void RegisterSettingsPage(PluginSettingsPageRegistration registration)
{
ArgumentNullException.ThrowIfNull(registration);
if (!_settingsPages.TryAdd(registration.Id, registration))
{
throw new InvalidOperationException(
$"Plugin '{Manifest.Id}' already registered a settings page with id '{registration.Id}'.");
}
}
public void RegisterDesktopComponent(PluginDesktopComponentRegistration registration)
{
ArgumentNullException.ThrowIfNull(registration);
if (!_desktopComponents.TryAdd(registration.ComponentId, registration))
{
throw new InvalidOperationException(
$"Plugin '{Manifest.Id}' already registered a desktop component with id '{registration.ComponentId}'.");
}
}
public IReadOnlyList<PluginSettingsPageRegistration> GetSettingsPagesSnapshot()
{
return _settingsPages.Values
.OrderBy(page => page.SortOrder)
.ThenBy(page => page.Title, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
public IReadOnlyList<PluginDesktopComponentRegistration> GetDesktopComponentsSnapshot()
{
return _desktopComponents.Values
.OrderBy(component => component.Category, StringComparer.OrdinalIgnoreCase)
.ThenBy(component => component.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
}
private sealed class NullServiceProvider : IServiceProvider
{
public static NullServiceProvider Instance { get; } = new();
private NullServiceProvider()
{
}
public object? GetService(Type serviceType)
{
return null;
}
}
private enum PluginSourceKind
{
Package = 0,
Manifest = 1
}
private sealed record PluginCandidate(
string SourcePath,
PluginManifest Manifest,
PluginSourceKind SourceKind);
}

View File

@@ -0,0 +1,21 @@
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginLoaderOptions
{
public string ManifestFileName { get; init; } = PluginSdkInfo.ManifestFileName;
public string PackageFileExtension { get; init; } = PluginSdkInfo.PackageFileExtension;
public string DataDirectoryName { get; init; } = PluginSdkInfo.DataDirectoryName;
public string RuntimeDirectoryName { get; init; } = PluginSdkInfo.RuntimeDirectoryName;
public string ExtractedPackagesDirectoryName { get; init; } = PluginSdkInfo.ExtractedPackagesDirectoryName;
public string PackagedDataDirectoryName { get; init; } = PluginSdkInfo.PackagedDataDirectoryName;
public ISet<string> SharedAssemblyNames { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
typeof(IPlugin).Assembly.GetName().Name!
};
}

View File

@@ -0,0 +1,107 @@
using System.Text.Json;
namespace LanMountainDesktop.PluginSdk;
public sealed record PluginManifest(
string Id,
string Name,
string EntranceAssembly,
string? Description = null,
string? Author = null,
string? Version = null,
string? ApiVersion = null)
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
};
public static PluginManifest Load(string manifestPath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath);
using var stream = File.OpenRead(manifestPath);
return Load(stream, manifestPath);
}
public static PluginManifest Load(Stream stream, string sourceName)
{
ArgumentNullException.ThrowIfNull(stream);
ArgumentException.ThrowIfNullOrWhiteSpace(sourceName);
var manifest = JsonSerializer.Deserialize<PluginManifest>(stream, SerializerOptions);
if (manifest is null)
{
throw new InvalidOperationException($"Failed to deserialize plugin manifest '{sourceName}'.");
}
return manifest.NormalizeAndValidate(sourceName);
}
public string ResolveEntranceAssemblyPath(string manifestPath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(manifestPath);
if (Path.IsPathRooted(EntranceAssembly))
{
return Path.GetFullPath(EntranceAssembly);
}
var manifestDirectory = Path.GetDirectoryName(Path.GetFullPath(manifestPath))
?? throw new InvalidOperationException($"Failed to determine the directory of '{manifestPath}'.");
return Path.GetFullPath(Path.Combine(manifestDirectory, EntranceAssembly));
}
private PluginManifest NormalizeAndValidate(string manifestPath)
{
var normalized = this with
{
Id = RequireValue(Id, nameof(Id), manifestPath),
Name = RequireValue(Name, nameof(Name), manifestPath),
EntranceAssembly = RequireValue(EntranceAssembly, nameof(EntranceAssembly), manifestPath),
Description = NormalizeOptionalValue(Description),
Author = NormalizeOptionalValue(Author),
Version = NormalizeOptionalValue(Version),
ApiVersion = NormalizeOptionalValue(ApiVersion) ?? PluginSdkInfo.ApiVersion
};
if (!System.Version.TryParse(normalized.ApiVersion, out var requestedVersion))
{
throw new InvalidOperationException(
$"Plugin manifest '{manifestPath}' declares an invalid API version '{normalized.ApiVersion}'.");
}
if (!System.Version.TryParse(PluginSdkInfo.ApiVersion, out var currentVersion))
{
throw new InvalidOperationException($"Plugin SDK API version '{PluginSdkInfo.ApiVersion}' is invalid.");
}
if (requestedVersion.Major != currentVersion.Major)
{
throw new InvalidOperationException(
$"Plugin '{normalized.Id}' targets API version '{normalized.ApiVersion}', but the host provides '{PluginSdkInfo.ApiVersion}'.");
}
return normalized;
}
private static string RequireValue(string? value, string propertyName, string manifestPath)
{
var normalized = NormalizeOptionalValue(value);
if (string.IsNullOrWhiteSpace(normalized))
{
throw new InvalidOperationException(
$"Plugin manifest '{manifestPath}' is missing required property '{propertyName}'.");
}
return normalized;
}
private static string? NormalizeOptionalValue(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}

View File

@@ -0,0 +1,12 @@
namespace LanMountainDesktop.PluginSdk;
public static class PluginSdkInfo
{
public const string ApiVersion = "1.0.0";
public const string ManifestFileName = "plugin.json";
public const string PackageFileExtension = ".laapp";
public const string DataDirectoryName = "Data";
public const string RuntimeDirectoryName = ".runtime";
public const string ExtractedPackagesDirectoryName = "packages";
public const string PackagedDataDirectoryName = "data";
}

View File

@@ -0,0 +1,30 @@
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginSettingsPageRegistration
{
public PluginSettingsPageRegistration(
string id,
string title,
Func<Control> contentFactory,
int sortOrder = 0)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ArgumentException.ThrowIfNullOrWhiteSpace(title);
ArgumentNullException.ThrowIfNull(contentFactory);
Id = id.Trim();
Title = title.Trim();
ContentFactory = contentFactory;
SortOrder = sortOrder;
}
public string Id { get; }
public string Title { get; }
public int SortOrder { get; }
public Func<Control> ContentFactory { get; }
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.0</Version>
<EnableDynamicLoading>true</EnableDynamicLoading>
<OutputPath>bin\$(Configuration)\$(TargetFramework)\content\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<PluginPackageOutputDirectory>..\LanMountainDesktop\bin\$(Configuration)\$(TargetFramework)\Extensions\Plugins\</PluginPackageOutputDirectory>
<PluginPackagePath>$(PluginPackageOutputDirectory)$(AssemblyName).laapp</PluginPackagePath>
<LegacyLoosePluginOutputDirectory>..\LanMountainDesktop\bin\$(Configuration)\$(TargetFramework)\Extensions\Plugins\SamplePlugin\</LegacyLoosePluginOutputDirectory>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" Private="false" />
<None Include="plugin.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<Target Name="CreateLaappPackage" AfterTargets="Build">
<MakeDir Directories="$(PluginPackageOutputDirectory)" />
<RemoveDir Directories="$(LegacyLoosePluginOutputDirectory)" />
<Delete Files="$(PluginPackagePath)" TreatErrorsAsWarnings="true" />
<ZipDirectory SourceDirectory="$(OutputPath)" DestinationFile="$(PluginPackagePath)" />
</Target>
</Project>

View File

@@ -0,0 +1,66 @@
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.SamplePlugin;
[PluginEntrance]
public sealed class SamplePlugin : PluginBase, IDisposable
{
private SamplePluginHeartbeatService? _heartbeatService;
public override void Initialize(IPluginContext context)
{
Directory.CreateDirectory(context.DataDirectory);
var hostName = context.TryGetProperty<string>("HostApplicationName", out var configuredHostName) &&
!string.IsNullOrWhiteSpace(configuredHostName)
? configuredHostName
: "UnknownHost";
var version = context.Manifest.Version ?? "dev";
SamplePluginRuntimeStatus.Reset(hostName, version, context.DataDirectory);
var message =
$"[{DateTimeOffset.UtcNow:O}] {context.Manifest.Name} initialized in {hostName} (plugin version {version}).";
try
{
File.AppendAllText(
Path.Combine(context.DataDirectory, "sample-plugin.log"),
message + Environment.NewLine);
SamplePluginRuntimeStatus.MarkBackendReady(
$"Plugin entry initialized successfully. Host: {hostName}; Version: {version}");
}
catch (Exception ex)
{
SamplePluginRuntimeStatus.MarkBackendFaulted($"Initialization log write failed: {ex.Message}");
throw;
}
_heartbeatService = new SamplePluginHeartbeatService(context.DataDirectory);
_heartbeatService.Start();
context.RegisterSettingsPage(new PluginSettingsPageRegistration(
"status",
"Plugin Status",
() => new SamplePluginSettingsView(context)));
context.RegisterDesktopComponent(new PluginDesktopComponentRegistration(
"LanMountainDesktop.SamplePlugin.StatusClock",
"Sample Plugin Status Clock",
widgetContext => new SamplePluginStatusClockWidget(widgetContext),
iconKey: "PuzzlePiece",
category: "Plugins",
minWidthCells: 4,
minHeightCells: 4,
allowDesktopPlacement: true,
allowStatusBarPlacement: false,
resizeMode: PluginDesktopComponentResizeMode.Proportional,
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.34, 18, 34)));
}
public void Dispose()
{
_heartbeatService?.Dispose();
_heartbeatService = null;
}
}

View File

@@ -0,0 +1,251 @@
using System.Globalization;
using System.IO;
using System.Threading;
namespace LanMountainDesktop.SamplePlugin;
internal enum SamplePluginHealthState
{
Healthy,
Pending,
Faulted
}
internal sealed record SamplePluginStatusEntry(
string Key,
string Title,
SamplePluginHealthState State,
string Summary,
string Detail,
DateTimeOffset UpdatedAt);
internal static class SamplePluginRuntimeStatus
{
private static readonly object Gate = new();
private static SamplePluginStatusEntry _frontend = CreateEntry(
"frontend",
"Frontend",
SamplePluginHealthState.Pending,
"Pending",
"Frontend surfaces have not been created yet.");
private static SamplePluginStatusEntry _component = CreateEntry(
"component",
"Component",
SamplePluginHealthState.Pending,
"Pending",
"The 4x4 component has not been created yet.");
private static SamplePluginStatusEntry _backend = CreateEntry(
"backend",
"Backend",
SamplePluginHealthState.Pending,
"Pending",
"Plugin initialization has not finished yet.");
private static SamplePluginStatusEntry _service = CreateEntry(
"service",
"Service",
SamplePluginHealthState.Pending,
"Pending",
"Heartbeat service has not started yet.");
public static void Reset(string hostName, string version, string dataDirectory)
{
lock (Gate)
{
_frontend = CreateEntry(
"frontend",
"Frontend",
SamplePluginHealthState.Pending,
"Pending",
"Waiting for the settings page or widget surface to render.");
_component = CreateEntry(
"component",
"Component",
SamplePluginHealthState.Pending,
"Pending",
"The 4x4 component has not been created yet.");
_backend = CreateEntry(
"backend",
"Backend",
SamplePluginHealthState.Healthy,
"Healthy",
$"Plugin initialized. Host: {hostName}; Version: {version}; Data: {dataDirectory}");
_service = CreateEntry(
"service",
"Service",
SamplePluginHealthState.Pending,
"Pending",
"Heartbeat service is starting.");
}
}
public static void MarkFrontendReady(string detail)
{
lock (Gate)
{
_frontend = CreateEntry(
"frontend",
"Frontend",
SamplePluginHealthState.Healthy,
"Healthy",
detail);
}
}
public static void MarkComponentCreated(string detail)
{
lock (Gate)
{
_component = CreateEntry(
"component",
"Component",
SamplePluginHealthState.Healthy,
"Created",
detail);
}
}
public static void MarkBackendReady(string detail)
{
lock (Gate)
{
_backend = CreateEntry(
"backend",
"Backend",
SamplePluginHealthState.Healthy,
"Healthy",
detail);
}
}
public static void MarkBackendFaulted(string detail)
{
lock (Gate)
{
_backend = CreateEntry(
"backend",
"Backend",
SamplePluginHealthState.Faulted,
"Faulted",
detail);
}
}
public static void MarkServiceHeartbeat(DateTimeOffset timestamp)
{
lock (Gate)
{
_service = CreateEntry(
"service",
"Service",
SamplePluginHealthState.Healthy,
"Healthy",
$"Heartbeat service is running. Last heartbeat: {timestamp.LocalDateTime:HH:mm:ss}");
}
}
public static void MarkServiceFaulted(string detail)
{
lock (Gate)
{
_service = CreateEntry(
"service",
"Service",
SamplePluginHealthState.Faulted,
"Faulted",
detail);
}
}
public static IReadOnlyList<SamplePluginStatusEntry> GetSnapshot()
{
lock (Gate)
{
return
[
_frontend,
_component,
_backend,
_service
];
}
}
private static SamplePluginStatusEntry CreateEntry(
string key,
string title,
SamplePluginHealthState state,
string summary,
string detail)
{
return new SamplePluginStatusEntry(
key,
title,
state,
summary,
detail,
DateTimeOffset.Now);
}
}
internal sealed class SamplePluginHeartbeatService : IDisposable
{
private readonly string _heartbeatFilePath;
private readonly Timer _timer;
private int _disposed;
public SamplePluginHeartbeatService(string dataDirectory)
{
Directory.CreateDirectory(dataDirectory);
_heartbeatFilePath = Path.Combine(dataDirectory, "service-heartbeat.txt");
_timer = new Timer(OnTimerTick);
}
public void Start()
{
PublishHeartbeat();
_timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
{
return;
}
_timer.Dispose();
}
private void OnTimerTick(object? state)
{
PublishHeartbeat();
}
private void PublishHeartbeat()
{
if (Volatile.Read(ref _disposed) != 0)
{
return;
}
var now = DateTimeOffset.Now;
try
{
File.WriteAllText(
_heartbeatFilePath,
now.ToString("O", CultureInfo.InvariantCulture));
SamplePluginRuntimeStatus.MarkServiceHeartbeat(now);
}
catch (Exception ex)
{
SamplePluginRuntimeStatus.MarkServiceFaulted($"Heartbeat write failed: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,190 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.SamplePlugin;
internal sealed class SamplePluginSettingsView : UserControl
{
private readonly DispatcherTimer _refreshTimer = new()
{
Interval = TimeSpan.FromSeconds(1)
};
private readonly IPluginContext _context;
private readonly TextBlock _summaryTextBlock;
private readonly StackPanel _statusPanel;
public SamplePluginSettingsView(IPluginContext context)
{
_context = context;
_summaryTextBlock = new TextBlock
{
Foreground = new SolidColorBrush(Color.Parse("#FFBAE6FD")),
TextWrapping = TextWrapping.Wrap
};
_statusPanel = new StackPanel
{
Spacing = 10
};
SamplePluginRuntimeStatus.MarkFrontendReady("Settings page rendered successfully.");
_refreshTimer.Tick += OnRefreshTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
Content = new Border
{
Background = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops =
[
new GradientStop(Color.Parse("#1F0B1120"), 0),
new GradientStop(Color.Parse("#260C4A6E"), 1)
]
},
BorderBrush = new SolidColorBrush(Color.Parse("#6628B2FF")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(18),
Padding = new Thickness(18),
Child = new StackPanel
{
Spacing = 14,
Children =
{
new TextBlock
{
Text = "Sample Plugin Runtime Status",
FontSize = 22,
FontWeight = FontWeight.SemiBold,
Foreground = Brushes.White
},
_summaryTextBlock,
new Border
{
Background = new SolidColorBrush(Color.Parse("#14000000")),
BorderBrush = new SolidColorBrush(Color.Parse("#3328B2FF")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14),
Child = _statusPanel
}
}
}
};
RefreshStatuses();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
RefreshStatuses();
_refreshTimer.Start();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_refreshTimer.Stop();
}
private void OnRefreshTimerTick(object? sender, EventArgs e)
{
RefreshStatuses();
}
private void RefreshStatuses()
{
_summaryTextBlock.Text =
$"Plugin Id: {_context.Manifest.Id}\nVersion: {_context.Manifest.Version ?? "dev"}\nData Path: {_context.DataDirectory}";
_statusPanel.Children.Clear();
foreach (var entry in SamplePluginRuntimeStatus.GetSnapshot())
{
var palette = GetPalette(entry.State);
_statusPanel.Children.Add(new Border
{
Background = new SolidColorBrush(palette.Background),
BorderBrush = new SolidColorBrush(palette.Border),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(12, 10),
Child = new StackPanel
{
Spacing = 4,
Children =
{
new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
ColumnSpacing = 8,
Children =
{
new Border
{
Width = 10,
Height = 10,
CornerRadius = new CornerRadius(999),
Background = new SolidColorBrush(palette.Dot),
VerticalAlignment = VerticalAlignment.Center
},
new TextBlock
{
Text = entry.Title,
FontSize = 15,
FontWeight = FontWeight.SemiBold,
Foreground = Brushes.White
},
new TextBlock
{
Text = entry.Summary,
Foreground = new SolidColorBrush(Color.Parse("#FFD7F2FF")),
HorizontalAlignment = HorizontalAlignment.Right
}
}
},
new TextBlock
{
Text = entry.Detail,
Foreground = new SolidColorBrush(Color.Parse("#FFE0F2FE")),
TextWrapping = TextWrapping.Wrap
},
new TextBlock
{
Text = $"Updated: {entry.UpdatedAt.LocalDateTime:HH:mm:ss}",
Foreground = new SolidColorBrush(Color.Parse("#FF93C5FD"))
}
}
}
});
var row = (Grid)((StackPanel)((Border)_statusPanel.Children[^1]).Child!).Children[0];
Grid.SetColumn(row.Children[1], 1);
Grid.SetColumn(row.Children[2], 2);
}
}
private static (Color Background, Color Border, Color Dot) GetPalette(SamplePluginHealthState state)
{
return state switch
{
SamplePluginHealthState.Healthy => (
Color.Parse("#1F115E59"),
Color.Parse("#665EEAD4"),
Color.Parse("#5EEAD4")),
SamplePluginHealthState.Faulted => (
Color.Parse("#291B1B"),
Color.Parse("#66F87171"),
Color.Parse("#F87171")),
_ => (
Color.Parse("#2B3A2A0D"),
Color.Parse("#66FBBF24"),
Color.Parse("#FBBF24"))
};
}
}

View File

@@ -0,0 +1,227 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.SamplePlugin;
internal sealed class SamplePluginStatusClockWidget : Border
{
private readonly DispatcherTimer _timer = new()
{
Interval = TimeSpan.FromSeconds(1)
};
private readonly PluginDesktopComponentContext _context;
private readonly TextBlock _timeTextBlock;
private readonly TextBlock _titleTextBlock;
private readonly StackPanel _statusPanel;
public SamplePluginStatusClockWidget(PluginDesktopComponentContext context)
{
_context = context;
_timeTextBlock = new TextBlock
{
Foreground = Brushes.White,
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Left
};
_titleTextBlock = new TextBlock
{
Text = "Plugin Status",
Foreground = new SolidColorBrush(Color.Parse("#FFBFE9FF")),
HorizontalAlignment = HorizontalAlignment.Left
};
_statusPanel = new StackPanel
{
Spacing = 8
};
Background = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops =
[
new GradientStop(Color.Parse("#FF07111F"), 0),
new GradientStop(Color.Parse("#FF0C4A6E"), 0.55),
new GradientStop(Color.Parse("#FF0EA5E9"), 1)
]
};
BorderBrush = new SolidColorBrush(Color.Parse("#6648C7FF"));
BorderThickness = new Thickness(1);
HorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment = VerticalAlignment.Stretch;
Child = new Grid
{
RowDefinitions = new RowDefinitions("Auto,*"),
RowSpacing = 14,
Children =
{
new StackPanel
{
Spacing = 4,
HorizontalAlignment = HorizontalAlignment.Left,
Children =
{
_timeTextBlock,
_titleTextBlock
}
},
new Border
{
Background = new SolidColorBrush(Color.Parse("#1F082F49")),
BorderBrush = new SolidColorBrush(Color.Parse("#5538BDF8")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(18),
Padding = new Thickness(12),
Child = _statusPanel
}
}
};
Grid.SetRow(((Grid)Child).Children[1], 1);
_timer.Tick += OnTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
var placementText = string.IsNullOrWhiteSpace(context.PlacementId)
? "Preview instance created."
: $"Widget created for placement {context.PlacementId}.";
SamplePluginRuntimeStatus.MarkFrontendReady("Widget frontend surface rendered successfully.");
SamplePluginRuntimeStatus.MarkComponentCreated($"{placementText} Baseline footprint: 4x4.");
RefreshClock();
RefreshStatusPanel();
ApplyScale();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
RefreshClock();
RefreshStatusPanel();
_timer.Start();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_timer.Stop();
}
private void OnTimerTick(object? sender, EventArgs e)
{
RefreshClock();
RefreshStatusPanel();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyScale();
RefreshStatusPanel();
}
private void RefreshClock()
{
_timeTextBlock.Text = DateTime.Now.ToString("HH:mm:ss");
}
private void RefreshStatusPanel()
{
_statusPanel.Children.Clear();
var basis = GetLayoutBasis();
var titleSize = Math.Clamp(basis * 0.072, 11, 16);
var detailSize = Math.Clamp(basis * 0.055, 10, 13);
foreach (var entry in SamplePluginRuntimeStatus.GetSnapshot())
{
var palette = GetPalette(entry.State);
var summaryText = $"{entry.Summary} - {entry.UpdatedAt.LocalDateTime:HH:mm:ss}";
_statusPanel.Children.Add(new Border
{
Background = new SolidColorBrush(palette.Background),
BorderBrush = new SolidColorBrush(palette.Border),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(10, 8),
Child = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
ColumnSpacing = 8,
Children =
{
new Border
{
Width = Math.Clamp(basis * 0.038, 8, 11),
Height = Math.Clamp(basis * 0.038, 8, 11),
CornerRadius = new CornerRadius(999),
Background = new SolidColorBrush(palette.Dot),
VerticalAlignment = VerticalAlignment.Center
},
new TextBlock
{
Text = entry.Title,
FontSize = titleSize,
FontWeight = FontWeight.SemiBold,
Foreground = Brushes.White,
TextWrapping = TextWrapping.Wrap
},
new TextBlock
{
Text = summaryText,
FontSize = detailSize,
Foreground = new SolidColorBrush(Color.Parse("#FFD7F2FF")),
HorizontalAlignment = HorizontalAlignment.Right,
TextAlignment = TextAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
}
}
}
});
var row = (Grid)((Border)_statusPanel.Children[^1]).Child!;
Grid.SetColumn(row.Children[1], 1);
Grid.SetColumn(row.Children[2], 2);
}
}
private void ApplyScale()
{
var basis = GetLayoutBasis();
Padding = new Thickness(Math.Clamp(basis * 0.09, 16, 26));
CornerRadius = new CornerRadius(Math.Clamp(basis * 0.14, 20, 34));
_timeTextBlock.FontSize = Math.Clamp(basis * 0.22, 30, 58);
_titleTextBlock.FontSize = Math.Clamp(basis * 0.07, 12, 18);
}
private double GetLayoutBasis()
{
var width = Bounds.Width > 1 ? Bounds.Width : _context.CellSize * 4;
var height = Bounds.Height > 1 ? Bounds.Height : _context.CellSize * 4;
return Math.Max(_context.CellSize * 4, Math.Min(width, height));
}
private static (Color Background, Color Border, Color Dot) GetPalette(SamplePluginHealthState state)
{
return state switch
{
SamplePluginHealthState.Healthy => (
Color.Parse("#1F0F766E"),
Color.Parse("#4D5EEAD4"),
Color.Parse("#5EEAD4")),
SamplePluginHealthState.Faulted => (
Color.Parse("#29B91C1C"),
Color.Parse("#66F87171"),
Color.Parse("#F87171")),
_ => (
Color.Parse("#1F7C2D12"),
Color.Parse("#66FDBA74"),
Color.Parse("#FDBA74"))
};
}
}

View File

@@ -0,0 +1,9 @@
{
"id": "LanMountainDesktop.SamplePlugin",
"name": "LanMountain Sample Plugin",
"description": "Example plugin used to validate PluginSdk loading and isolation.",
"author": "LanMountainDesktop",
"version": "1.0.0",
"apiVersion": "1.0.0",
"entranceAssembly": "LanMountainDesktop.SamplePlugin.dll"
}

View File

@@ -5,6 +5,10 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop", "LanMountainDesktop\LanMountainDesktop.csproj", "{00000001-0000-0000-0000-000000000001}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.SamplePlugin", "LanMountainDesktop.SamplePlugin\LanMountainDesktop.SamplePlugin.csproj", "{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.PluginSdk", "LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj", "{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,5 +19,13 @@ Global
{00000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00000001-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00000001-0000-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}.Release|Any CPU.Build.0 = Release|Any CPU
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -22,6 +22,8 @@
ToolTipText="LanMountainDesktop">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="设置" Click="OnTraySettingsClick" />
<NativeMenuItemSeparator />
<NativeMenuItem Header="重启应用" Click="OnTrayRestartClick" />
<NativeMenuItemSeparator />
<NativeMenuItem Header="退出应用" Click="OnTrayExitClick" />

View File

@@ -6,6 +6,7 @@ using System;
using System.Diagnostics;
using System.Linq;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LanMountainDesktop.Services;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
@@ -15,6 +16,11 @@ namespace LanMountainDesktop;
public partial class App : Application
{
private SettingsWindow? _traySettingsWindow;
private PluginRuntimeService? _pluginRuntimeService;
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
public override void Initialize()
{
ConfigureWebViewUserDataFolder();
@@ -25,12 +31,14 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
LinuxDesktopEntryInstaller.EnsureInstalled();
InitializePluginRuntime();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
@@ -48,6 +56,44 @@ public partial class App : Application
}
}
private void OnTraySettingsClick(object? sender, EventArgs e)
{
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
try
{
if (_traySettingsWindow is { } existingWindow && existingWindow.IsVisible)
{
existingWindow.WindowState = Avalonia.Controls.WindowState.Normal;
existingWindow.Activate();
return;
}
var settingsWindow = new SettingsWindow();
settingsWindow.Closed += (_, _) =>
{
if (ReferenceEquals(_traySettingsWindow, settingsWindow))
{
_traySettingsWindow = null;
}
};
_traySettingsWindow = settingsWindow;
settingsWindow.Show();
settingsWindow.Activate();
}
catch (Exception ex)
{
Debug.WriteLine($"[TraySettings] Failed to open settings window: {ex}");
}
}, DispatcherPriority.Normal);
}
private void OnTrayRestartClick(object? sender, EventArgs e)
{
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
@@ -130,4 +176,18 @@ public partial class App : Application
// Keep startup resilient if user profile folders are unavailable.
}
}
private void InitializePluginRuntime()
{
try
{
_pluginRuntimeService?.Dispose();
_pluginRuntimeService = new PluginRuntimeService();
_pluginRuntimeService.LoadInstalledPlugins();
}
catch (Exception ex)
{
Debug.WriteLine($"[PluginRuntime] Failed to initialize plugin runtime: {ex}");
}
}
}

View File

@@ -385,6 +385,13 @@ public sealed class ComponentRegistry
return new ComponentRegistry(merged);
}
public ComponentRegistry RegisterComponents(IEnumerable<DesktopComponentDefinition> definitions)
{
var merged = _definitions.Values.ToList();
merged.AddRange(definitions);
return new ComponentRegistry(merged);
}
public bool TryGetDefinition(string componentId, out DesktopComponentDefinition definition)
{
return _definitions.TryGetValue(componentId, out definition!);

View File

@@ -0,0 +1,8 @@
using LanMountainDesktop.Services;
namespace LanMountainDesktop.ComponentSystem;
public sealed record DesktopComponentRuntimeContext(
string ComponentId,
string? PlacementId,
IComponentInstanceSettingsStore ComponentSettingsStore);

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.ComponentSystem;
public interface IComponentPlacementContextAware
{
void SetComponentPlacementContext(string componentId, string? placementId);
}

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.ComponentSystem;
public interface IComponentRuntimeContextAware
{
void SetComponentRuntimeContext(DesktopComponentRuntimeContext context);
}

View File

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

View File

@@ -24,6 +24,10 @@
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />

View File

@@ -268,9 +268,30 @@
"settings.launcher.restore_button": "Show Again",
"settings.plugins.title": "Plugins",
"settings.plugins.runtime_header": "Plugin Runtime",
"settings.plugins.runtime_desc": "Manage plugin loading and backend isolation.",
"settings.plugins.runtime_hint": "This page will host installed plugin management, permission review, and sandboxed backend runtime controls.",
"settings.plugins.runtime_status": "Plugin management UI is not connected yet. Next step is wiring the loader, permissions, and worker isolation state into this panel.",
"settings.plugins.runtime_desc": "Review plugin runtime state and load results.",
"settings.plugins.runtime_hint": "This page shows discovery status, load results, and runtime diagnostics for installed plugins.",
"settings.plugins.runtime_status": "Plugin runtime status will appear here after plugin discovery completes.",
"settings.plugins.installed_header": "Installed Plugins",
"settings.plugins.installed_desc": "Enable or disable plugins here. Detailed plugin settings appear as separate settings pages.",
"settings.plugins.restart_hint": "Plugin enable state changes take effect after restarting the app.",
"settings.plugins.empty": "No plugins found.",
"settings.plugins.runtime_unavailable": "Plugin runtime is not available.",
"settings.plugins.summary_format": "Detected {0} plugin(s); enabled {1}; loaded {2}; settings pages {3}; widgets {4}; failures {5}.",
"settings.plugins.summary_item_format": "{0} v{1} | {2}",
"settings.plugins.state.enabled": "Enabled",
"settings.plugins.state.enabled_failed": "Enabled / failed to load",
"settings.plugins.state.disabled": "Disabled",
"settings.plugins.state.loaded": "Loaded",
"settings.plugins.state.load_failed": "Load failed",
"settings.plugins.toggle_on": "Enabled",
"settings.plugins.toggle_off": "Disabled",
"settings.plugins.toggle_result_format": "Plugin '{0}' was {1} for the next launch. Restart the app to apply page and widget changes.",
"settings.plugins.toggle_state_enabled": "enabled",
"settings.plugins.toggle_state_disabled": "disabled",
"settings.plugins.source_package": ".laapp package",
"settings.plugins.source_manifest": "Loose manifest",
"settings.plugins.subtitle_format": "{0} | {1} | {2}",
"settings.plugins.detail_format": "Settings pages: {0} | Widgets: {1}",
"button.component_library": "Edit Desktop",
"tooltip.component_library": "Edit Desktop",
"component_library.title": "Widgets",
@@ -617,3 +638,5 @@
"placement.center": "Center",
"placement.tile": "Tile"
}

View File

@@ -268,9 +268,30 @@
"settings.launcher.restore_button": "重新显示",
"settings.plugins.title": "插件",
"settings.plugins.runtime_header": "插件运行时",
"settings.plugins.runtime_desc": "管理插件加载与后端隔离运行。",
"settings.plugins.runtime_hint": "这里将承载已安装插件、权限审查和沙盒后端运行时控制。",
"settings.plugins.runtime_status": "插件管理界面尚未接入实际数据。下一步是把加载器、权限和 worker 隔离状态接到这里。",
"settings.plugins.runtime_desc": "查看插件运行时状态、加载结果与诊断信息。",
"settings.plugins.runtime_hint": "这里展示已安装插件的发现结果、加载状态和运行时诊断信息。",
"settings.plugins.runtime_status": "插件扫描完成后,运行时状态会显示在这里。",
"settings.plugins.installed_header": "已安装插件",
"settings.plugins.installed_desc": "在这里启用或禁用插件。插件自己的详细设置会作为独立设置页出现。",
"settings.plugins.restart_hint": "插件启用状态变更会在重启应用后生效。",
"settings.plugins.empty": "未找到插件。",
"settings.plugins.runtime_unavailable": "插件运行时不可用。",
"settings.plugins.summary_format": "共检测到 {0} 个插件;已启用 {1} 个;已加载 {2} 个;设置页 {3} 个;组件 {4} 个;失败 {5} 个。",
"settings.plugins.summary_item_format": "{0} v{1} | {2}",
"settings.plugins.state.enabled": "已启用",
"settings.plugins.state.enabled_failed": "已启用 / 加载失败",
"settings.plugins.state.disabled": "已禁用",
"settings.plugins.state.loaded": "已加载",
"settings.plugins.state.load_failed": "加载失败",
"settings.plugins.toggle_on": "启用",
"settings.plugins.toggle_off": "禁用",
"settings.plugins.toggle_result_format": "插件“{0}”已在下次启动时设为{1}。重启应用后,设置页和组件变更才会生效。",
"settings.plugins.toggle_state_enabled": "启用",
"settings.plugins.toggle_state_disabled": "禁用",
"settings.plugins.source_package": ".laapp 包",
"settings.plugins.source_manifest": "散装清单",
"settings.plugins.subtitle_format": "{0} | {1} | {2}",
"settings.plugins.detail_format": "设置页:{0} | 组件:{1}",
"button.component_library": "桌面编辑",
"tooltip.component_library": "桌面编辑",
"component_library.title": "桌面编辑",
@@ -617,3 +638,5 @@
"placement.center": "居中",
"placement.tile": "平铺"
}

View File

@@ -20,6 +20,8 @@ public sealed class AppSettingsSnapshot
public int SettingsTabIndex { get; set; } = 0;
public string? SettingsTabTag { get; set; }
public string LanguageCode { get; set; } = "zh-CN";
public string? TimeZoneId { get; set; }
@@ -70,15 +72,7 @@ public sealed class AppSettingsSnapshot
public int StatusBarCustomSpacingPercent { get; set; } = 12;
public int DesktopPageCount { get; set; } = 1;
public int CurrentDesktopSurfaceIndex { get; set; } = 0;
public List<DesktopComponentPlacementSnapshot> DesktopComponentPlacements { get; set; } = [];
public List<string> HiddenLauncherFolderPaths { get; set; } = [];
public List<string> HiddenLauncherAppPaths { get; set; } = [];
public List<string> DisabledPluginIds { get; set; } = [];
public AppSettingsSnapshot Clone()
{
@@ -90,35 +84,8 @@ public sealed class AppSettingsSnapshot
clone.PinnedTaskbarActions = PinnedTaskbarActions is { Count: > 0 }
? new List<string>(PinnedTaskbarActions)
: [];
var placements = new List<DesktopComponentPlacementSnapshot>(DesktopComponentPlacements?.Count ?? 0);
if (DesktopComponentPlacements is not null)
{
foreach (var placement in DesktopComponentPlacements)
{
if (placement is null)
{
continue;
}
placements.Add(new DesktopComponentPlacementSnapshot
{
PlacementId = placement.PlacementId,
PageIndex = placement.PageIndex,
ComponentId = placement.ComponentId,
Row = placement.Row,
Column = placement.Column,
WidthCells = placement.WidthCells,
HeightCells = placement.HeightCells
});
}
}
clone.DesktopComponentPlacements = placements;
clone.HiddenLauncherFolderPaths = HiddenLauncherFolderPaths is { Count: > 0 }
? new List<string>(HiddenLauncherFolderPaths)
: [];
clone.HiddenLauncherAppPaths = HiddenLauncherAppPaths is { Count: > 0 }
? new List<string>(HiddenLauncherAppPaths)
clone.DisabledPluginIds = DisabledPluginIds is { Count: > 0 }
? new List<string>(DisabledPluginIds)
: [];
return clone;

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace LanMountainDesktop.Models;
public sealed class DesktopLayoutSettingsSnapshot
{
public int DesktopPageCount { get; set; } = 1;
public int CurrentDesktopSurfaceIndex { get; set; }
public List<DesktopComponentPlacementSnapshot> DesktopComponentPlacements { get; set; } = [];
public DesktopLayoutSettingsSnapshot Clone()
{
var clone = (DesktopLayoutSettingsSnapshot)MemberwiseClone();
var placements = new List<DesktopComponentPlacementSnapshot>(DesktopComponentPlacements?.Count ?? 0);
if (DesktopComponentPlacements is not null)
{
foreach (var placement in DesktopComponentPlacements)
{
if (placement is null)
{
continue;
}
placements.Add(new DesktopComponentPlacementSnapshot
{
PlacementId = placement.PlacementId,
PageIndex = placement.PageIndex,
ComponentId = placement.ComponentId,
Row = placement.Row,
Column = placement.Column,
WidthCells = placement.WidthCells,
HeightCells = placement.HeightCells
});
}
}
clone.DesktopComponentPlacements = placements;
return clone;
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace LanMountainDesktop.Models;
public sealed class LauncherSettingsSnapshot
{
public List<string> HiddenLauncherFolderPaths { get; set; } = [];
public List<string> HiddenLauncherAppPaths { get; set; } = [];
public LauncherSettingsSnapshot Clone()
{
var clone = (LauncherSettingsSnapshot)MemberwiseClone();
clone.HiddenLauncherFolderPaths = HiddenLauncherFolderPaths is { Count: > 0 }
? new List<string>(HiddenLauncherFolderPaths)
: [];
clone.HiddenLauncherAppPaths = HiddenLauncherAppPaths is { Count: > 0 }
? new List<string>(HiddenLauncherAppPaths)
: [];
return clone;
}
}

View File

@@ -7,6 +7,8 @@ namespace LanMountainDesktop.Services;
public sealed class AppSettingsService
{
public static event Action<string>? SettingsSaved;
private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = true
@@ -21,6 +23,8 @@ public sealed class AppSettingsService
private readonly string _settingsPath;
public string InstanceId { get; } = Guid.NewGuid().ToString("N");
public AppSettingsService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
@@ -88,6 +92,8 @@ public sealed class AppSettingsService
{
UpdateCache(snapshotToPersist, writeTimeUtc, DateTime.UtcNow);
}
SettingsSaved?.Invoke(InstanceId);
}
catch
{

View File

@@ -2,12 +2,13 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using LanMountainDesktop.Models;
namespace LanMountainDesktop.Services;
public sealed class ComponentSettingsService
public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
@@ -18,12 +19,14 @@ public sealed class ComponentSettingsService
private static readonly TimeSpan CacheProbeInterval = TimeSpan.FromMilliseconds(400);
private static string? _cachedPath;
private static ComponentSettingsSnapshot? _cachedSnapshot;
private static ComponentSettingsDocumentSnapshot? _cachedSnapshot;
private static DateTime _cachedWriteTimeUtc = DateTime.MinValue;
private static DateTime _lastProbeUtc = DateTime.MinValue;
private readonly string _settingsPath;
private readonly string _legacyAppSettingsPath;
private string _scopedComponentId = string.Empty;
private string _scopedPlacementId = string.Empty;
public ComponentSettingsService()
{
@@ -35,51 +38,17 @@ public sealed class ComponentSettingsService
public ComponentSettingsSnapshot Load()
{
if (HasScopedComponentContext())
{
return LoadForComponent(_scopedComponentId, _scopedPlacementId);
}
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;
}
ComponentSettingsSnapshot loadedSnapshot;
var loadedFromLegacy = false;
if (hasFile)
{
loadedSnapshot = LoadSnapshotFromDisk();
}
else if (TryLoadLegacySnapshot(out var migratedSnapshot))
{
loadedSnapshot = migratedSnapshot;
loadedFromLegacy = true;
}
else
{
loadedSnapshot = new ComponentSettingsSnapshot();
}
var normalizedSnapshot = NormalizeSnapshot(loadedSnapshot);
if (loadedFromLegacy)
{
writeTimeUtc = PersistSnapshotToDisk(normalizedSnapshot);
}
UpdateCache(normalizedSnapshot, writeTimeUtc, nowUtc);
return normalizedSnapshot.Clone();
var document = LoadDocumentLocked();
return document.DefaultSettings.Clone();
}
}
catch
@@ -90,15 +59,21 @@ public sealed class ComponentSettingsService
public void Save(ComponentSettingsSnapshot snapshot)
{
if (HasScopedComponentContext())
{
SaveForComponent(_scopedComponentId, _scopedPlacementId, snapshot);
return;
}
var snapshotToPersist = NormalizeSnapshot(snapshot);
try
{
var writeTimeUtc = PersistSnapshotToDisk(snapshotToPersist);
lock (CacheGate)
{
UpdateCache(snapshotToPersist, writeTimeUtc, DateTime.UtcNow);
var document = LoadDocumentLocked();
document.DefaultSettings = snapshotToPersist;
PersistDocumentLocked(document);
}
}
catch
@@ -107,7 +82,201 @@ public sealed class ComponentSettingsService
}
}
private bool TryGetCachedWithoutProbe(DateTime nowUtc, out ComponentSettingsSnapshot snapshot)
public ComponentSettingsSnapshot LoadForComponent(string componentId, string? placementId)
{
try
{
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 document.DefaultSettings.Clone();
}
}
catch
{
return new ComponentSettingsSnapshot();
}
}
public void SaveForComponent(string componentId, string? placementId, ComponentSettingsSnapshot snapshot)
{
var normalizedSnapshot = NormalizeSnapshot(snapshot);
var instanceKey = BuildInstanceKey(componentId, placementId);
if (string.IsNullOrWhiteSpace(instanceKey))
{
Save(normalizedSnapshot);
return;
}
try
{
lock (CacheGate)
{
var document = LoadDocumentLocked();
document.InstanceSettings[instanceKey] = normalizedSnapshot;
PersistDocumentLocked(document);
}
}
catch
{
// Swallow persistence errors to keep UI interactions uninterrupted.
}
}
public void DeleteForComponent(string componentId, string? placementId)
{
var instanceKey = BuildInstanceKey(componentId, placementId);
if (string.IsNullOrWhiteSpace(instanceKey))
{
return;
}
try
{
lock (CacheGate)
{
var document = LoadDocumentLocked();
var changed = document.InstanceSettings.Remove(instanceKey);
changed |= document.PluginSettings.Remove(instanceKey);
if (changed)
{
PersistDocumentLocked(document);
}
}
}
catch
{
// Swallow persistence errors to keep UI interactions uninterrupted.
}
}
public T LoadPluginSettings<T>(string componentId, string? placementId) where T : new()
{
try
{
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 JsonSerializer.Deserialize<T>(settingsElement.GetRawText(), SerializerOptions) ?? new T();
}
}
catch
{
return new T();
}
}
public void SavePluginSettings<T>(string componentId, string? placementId, T settings)
{
var instanceKey = BuildInstanceKey(componentId, placementId);
if (string.IsNullOrWhiteSpace(instanceKey))
{
return;
}
try
{
lock (CacheGate)
{
var document = LoadDocumentLocked();
document.PluginSettings[instanceKey] = JsonSerializer.SerializeToElement(settings, SerializerOptions).Clone();
PersistDocumentLocked(document);
}
}
catch
{
// Swallow persistence errors to keep UI interactions uninterrupted.
}
}
public void DeletePluginSettings(string componentId, string? placementId)
{
var instanceKey = BuildInstanceKey(componentId, placementId);
if (string.IsNullOrWhiteSpace(instanceKey))
{
return;
}
try
{
lock (CacheGate)
{
var document = LoadDocumentLocked();
if (document.PluginSettings.Remove(instanceKey))
{
PersistDocumentLocked(document);
}
}
}
catch
{
// Swallow persistence errors to keep UI interactions uninterrupted.
}
}
public void SetScopedComponentContext(string componentId, string? placementId)
{
_scopedComponentId = componentId?.Trim() ?? string.Empty;
_scopedPlacementId = placementId?.Trim() ?? string.Empty;
}
public void ClearScopedComponentContext()
{
_scopedComponentId = string.Empty;
_scopedPlacementId = string.Empty;
}
public static void ApplyScopedContextToTarget(object? target, string componentId, string? placementId)
{
if (target is null)
{
return;
}
var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
foreach (var field in target.GetType().GetFields(flags))
{
if (field.FieldType != typeof(ComponentSettingsService))
{
continue;
}
if (field.GetValue(target) is ComponentSettingsService settingsService)
{
settingsService.SetScopedComponentContext(componentId, placementId);
}
}
foreach (var property in target.GetType().GetProperties(flags))
{
if (property.PropertyType != typeof(ComponentSettingsService) ||
!property.CanRead)
{
continue;
}
if (property.GetValue(target) is ComponentSettingsService settingsService)
{
settingsService.SetScopedComponentContext(componentId, placementId);
}
}
}
private bool TryGetCachedWithoutProbe(DateTime nowUtc, out ComponentSettingsDocumentSnapshot snapshot)
{
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) &&
_cachedSnapshot is not null &&
@@ -121,7 +290,7 @@ public sealed class ComponentSettingsService
return false;
}
private bool TryGetCachedAfterProbe(DateTime writeTimeUtc, out ComponentSettingsSnapshot snapshot)
private bool TryGetCachedAfterProbe(DateTime writeTimeUtc, out ComponentSettingsDocumentSnapshot snapshot)
{
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) &&
_cachedSnapshot is not null &&
@@ -135,17 +304,78 @@ public sealed class ComponentSettingsService
return false;
}
private ComponentSettingsSnapshot LoadSnapshotFromDisk()
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 loadedFromLegacy = false;
if (hasFile)
{
loadedSnapshot = LoadSnapshotFromDisk();
}
else if (TryLoadLegacySnapshot(out var migratedSnapshot))
{
loadedSnapshot = new ComponentSettingsDocumentSnapshot
{
DefaultSettings = NormalizeSnapshot(migratedSnapshot)
};
loadedFromLegacy = true;
}
else
{
loadedSnapshot = new ComponentSettingsDocumentSnapshot();
}
var normalizedSnapshot = NormalizeDocument(loadedSnapshot);
if (loadedFromLegacy)
{
writeTimeUtc = PersistSnapshotToDisk(normalizedSnapshot);
}
UpdateCache(normalizedSnapshot, writeTimeUtc, nowUtc);
return normalizedSnapshot.Clone();
}
private ComponentSettingsDocumentSnapshot LoadSnapshotFromDisk()
{
try
{
var json = File.ReadAllText(_settingsPath);
var snapshot = JsonSerializer.Deserialize<ComponentSettingsSnapshot>(json, SerializerOptions);
return NormalizeSnapshot(snapshot);
using var document = JsonDocument.Parse(json);
if (document.RootElement.ValueKind == JsonValueKind.Object &&
(document.RootElement.TryGetProperty("defaultSettings", out _) ||
document.RootElement.TryGetProperty("instanceSettings", out _) ||
document.RootElement.TryGetProperty("pluginSettings", out _)))
{
var snapshot = JsonSerializer.Deserialize<ComponentSettingsDocumentSnapshot>(json, SerializerOptions);
return NormalizeDocument(snapshot);
}
var legacySnapshot = JsonSerializer.Deserialize<ComponentSettingsSnapshot>(json, SerializerOptions);
return new ComponentSettingsDocumentSnapshot
{
DefaultSettings = NormalizeSnapshot(legacySnapshot)
};
}
catch
{
return new ComponentSettingsSnapshot();
return new ComponentSettingsDocumentSnapshot();
}
}
@@ -204,7 +434,13 @@ public sealed class ComponentSettingsService
}
}
private DateTime PersistSnapshotToDisk(ComponentSettingsSnapshot snapshot)
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))
@@ -257,6 +493,40 @@ public sealed class ComponentSettingsService
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)
{
@@ -360,7 +630,30 @@ public sealed class ComponentSettingsService
return RefreshIntervalCatalog.Normalize(minutes, 20);
}
private void UpdateCache(ComponentSettingsSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc)
private static string BuildInstanceKey(string componentId, string? placementId)
{
var normalizedComponentId = componentId?.Trim() ?? string.Empty;
var normalizedPlacementId = placementId?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(normalizedComponentId) || string.IsNullOrWhiteSpace(normalizedPlacementId))
{
return string.Empty;
}
return $"{normalizedComponentId}::{normalizedPlacementId}";
}
private static string NormalizeInstanceKey(string? key)
{
return key?.Trim() ?? string.Empty;
}
private bool HasScopedComponentContext()
{
return !string.IsNullOrWhiteSpace(_scopedComponentId) &&
!string.IsNullOrWhiteSpace(_scopedPlacementId);
}
private void UpdateCache(ComponentSettingsDocumentSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc)
{
_cachedPath = _settingsPath;
_cachedSnapshot = snapshot.Clone();
@@ -368,6 +661,39 @@ public sealed class ComponentSettingsService
_lastProbeUtc = probeTimeUtc;
}
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;

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.ComponentSystem.Extensions;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Services;
public static class DesktopComponentRegistryFactory
{
public static ComponentRegistry Create(PluginRuntimeService? pluginRuntimeService)
{
var registry = ComponentRegistry
.CreateDefault()
.RegisterExtensions(
JsonComponentExtensionProvider.LoadProvidersFromDirectory(
Path.Combine(AppContext.BaseDirectory, "Extensions", "Components")));
var pluginDefinitions = GetPluginDefinitions(registry, pluginRuntimeService);
return pluginDefinitions.Count == 0
? registry
: registry.RegisterComponents(pluginDefinitions);
}
public static DesktopComponentRuntimeRegistry CreateRuntimeRegistry(
ComponentRegistry componentRegistry,
PluginRuntimeService? pluginRuntimeService)
{
var registrations = DesktopComponentRuntimeRegistry.GetDefaultRegistrations().ToList();
var registeredIds = new HashSet<string>(
registrations.Select(registration => registration.ComponentId),
StringComparer.OrdinalIgnoreCase);
if (pluginRuntimeService is not null)
{
foreach (var contribution in pluginRuntimeService.DesktopComponents)
{
var registration = contribution.Registration;
if (!componentRegistry.TryGetDefinition(registration.ComponentId, out _))
{
continue;
}
if (!registeredIds.Add(registration.ComponentId))
{
Debug.WriteLine(
$"[PluginRuntime] Skipped plugin widget '{registration.ComponentId}' from '{contribution.Plugin.Manifest.Id}' because a runtime registration already exists.");
continue;
}
registrations.Add(new DesktopComponentRuntimeRegistration(
registration.ComponentId,
registration.DisplayNameLocalizationKey,
factoryContext => CreatePluginControl(contribution, factoryContext),
registration.CornerRadiusResolver));
}
}
return new DesktopComponentRuntimeRegistry(componentRegistry, registrations);
}
private static List<DesktopComponentDefinition> GetPluginDefinitions(
ComponentRegistry baseRegistry,
PluginRuntimeService? pluginRuntimeService)
{
var definitions = new List<DesktopComponentDefinition>();
if (pluginRuntimeService is null)
{
return definitions;
}
var knownIds = new HashSet<string>(
baseRegistry.GetAll().Select(definition => definition.Id),
StringComparer.OrdinalIgnoreCase);
foreach (var contribution in pluginRuntimeService.DesktopComponents)
{
var registration = contribution.Registration;
if (!knownIds.Add(registration.ComponentId))
{
Debug.WriteLine(
$"[PluginRuntime] Skipped plugin widget '{registration.ComponentId}' from '{contribution.Plugin.Manifest.Id}' because the component id already exists.");
continue;
}
definitions.Add(new DesktopComponentDefinition(
registration.ComponentId,
registration.DisplayName,
registration.IconKey,
registration.Category,
registration.MinWidthCells,
registration.MinHeightCells,
registration.AllowStatusBarPlacement,
registration.AllowDesktopPlacement,
registration.ResizeMode == PluginDesktopComponentResizeMode.Free
? DesktopComponentResizeMode.Free
: DesktopComponentResizeMode.Proportional));
}
return definitions;
}
private static Control CreatePluginControl(
PluginDesktopComponentContribution contribution,
DesktopComponentControlFactoryContext context)
{
try
{
var pluginContext = new PluginDesktopComponentContext(
contribution.Plugin.Manifest,
contribution.Plugin.Context.PluginDirectory,
contribution.Plugin.Context.DataDirectory,
contribution.Plugin.Context.Services,
contribution.Plugin.Context.Properties,
contribution.Registration.ComponentId,
context.PlacementId,
context.CellSize);
return contribution.Registration.ControlFactory(pluginContext);
}
catch (Exception ex)
{
Debug.WriteLine(
$"[PluginRuntime] Failed to create widget '{contribution.Registration.ComponentId}' from '{contribution.Plugin.Manifest.Id}': {ex}");
return CreatePluginErrorControl(contribution, ex);
}
}
private static Control CreatePluginErrorControl(
PluginDesktopComponentContribution contribution,
Exception exception)
{
return new Border
{
Background = new SolidColorBrush(Color.Parse("#332B0F16")),
BorderBrush = new SolidColorBrush(Color.Parse("#66F97316")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(12),
Child = new StackPanel
{
Spacing = 6,
Children =
{
new TextBlock
{
Text = contribution.Registration.DisplayName,
FontSize = 14,
FontWeight = FontWeight.SemiBold,
TextWrapping = TextWrapping.Wrap
},
new TextBlock
{
Text = $"Plugin {contribution.Plugin.Manifest.Name} failed to create this widget.",
TextWrapping = TextWrapping.Wrap
},
new TextBlock
{
Text = exception.Message,
TextWrapping = TextWrapping.Wrap
}
}
}
};
}
}

View File

@@ -0,0 +1,248 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using LanMountainDesktop.Models;
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");
}
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
{
return new DesktopLayoutSettingsSnapshot();
}
}
public void Save(DesktopLayoutSettingsSnapshot snapshot)
{
var snapshotToPersist = NormalizeSnapshot(snapshot);
try
{
var writeTimeUtc = PersistSnapshotToDisk(snapshotToPersist);
lock (CacheGate)
{
UpdateCache(snapshotToPersist, writeTimeUtc, DateTime.UtcNow);
}
}
catch
{
// Swallow persistence errors to keep UI interactions uninterrupted.
}
}
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
{
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
{
return false;
}
}
private DateTime PersistSnapshotToDisk(DesktopLayoutSettingsSnapshot snapshot)
{
var directory = Path.GetDirectoryName(_settingsPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(snapshot, SerializerOptions);
File.WriteAllText(_settingsPath, json);
return File.Exists(_settingsPath)
? File.GetLastWriteTimeUtc(_settingsPath)
: DateTime.UtcNow;
}
private static DesktopLayoutSettingsSnapshot NormalizeSnapshot(DesktopLayoutSettingsSnapshot? snapshot)
{
var normalized = snapshot?.Clone() ?? new DesktopLayoutSettingsSnapshot();
normalized.DesktopPageCount = Math.Max(1, normalized.DesktopPageCount);
normalized.CurrentDesktopSurfaceIndex = Math.Max(0, normalized.CurrentDesktopSurfaceIndex);
var placements = new List<DesktopComponentPlacementSnapshot>(normalized.DesktopComponentPlacements?.Count ?? 0);
if (normalized.DesktopComponentPlacements is not null)
{
foreach (var placement in normalized.DesktopComponentPlacements)
{
if (placement is null)
{
continue;
}
placements.Add(new DesktopComponentPlacementSnapshot
{
PlacementId = placement.PlacementId?.Trim() ?? string.Empty,
PageIndex = Math.Max(0, placement.PageIndex),
ComponentId = placement.ComponentId?.Trim() ?? string.Empty,
Row = Math.Max(0, placement.Row),
Column = Math.Max(0, placement.Column),
WidthCells = Math.Max(1, placement.WidthCells),
HeightCells = Math.Max(1, placement.HeightCells)
});
}
}
normalized.DesktopComponentPlacements = placements;
return normalized;
}
private void UpdateCache(DesktopLayoutSettingsSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc)
{
_cachedPath = _settingsPath;
_cachedSnapshot = snapshot.Clone();
_cachedWriteTimeUtc = writeTimeUtc;
_lastProbeUtc = probeTimeUtc;
}
private sealed class LegacyDesktopLayoutSettingsSnapshot
{
public int DesktopPageCount { get; set; } = 1;
public int CurrentDesktopSurfaceIndex { get; set; }
public List<DesktopComponentPlacementSnapshot>? DesktopComponentPlacements { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using LanMountainDesktop.Models;
namespace LanMountainDesktop.Services;
public interface IComponentInstanceSettingsStore
{
ComponentSettingsSnapshot Load();
void Save(ComponentSettingsSnapshot snapshot);
ComponentSettingsSnapshot LoadForComponent(string componentId, string? placementId);
void SaveForComponent(string componentId, string? placementId, ComponentSettingsSnapshot snapshot);
void DeleteForComponent(string componentId, string? placementId);
T LoadPluginSettings<T>(string componentId, string? placementId) where T : new();
void SavePluginSettings<T>(string componentId, string? placementId, T settings);
void DeletePluginSettings(string componentId, string? placementId);
}

View File

@@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using LanMountainDesktop.Models;
namespace LanMountainDesktop.Services;
public sealed class LauncherSettingsService
{
public static event Action<string>? SettingsSaved;
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 LauncherSettingsSnapshot? _cachedSnapshot;
private static DateTime _cachedWriteTimeUtc = DateTime.MinValue;
private static DateTime _lastProbeUtc = DateTime.MinValue;
private readonly string _settingsPath;
private readonly string _legacyAppSettingsPath;
public string InstanceId { get; } = Guid.NewGuid().ToString("N");
public LauncherSettingsService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var settingsDirectory = Path.Combine(appData, "LanMountainDesktop");
_settingsPath = Path.Combine(settingsDirectory, "launcher-settings.json");
_legacyAppSettingsPath = Path.Combine(settingsDirectory, "settings.json");
}
public LauncherSettingsSnapshot 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;
}
LauncherSettingsSnapshot loadedSnapshot;
var loadedFromLegacy = false;
if (hasFile)
{
loadedSnapshot = LoadSnapshotFromDisk();
}
else if (TryLoadLegacySnapshot(out var migratedSnapshot))
{
loadedSnapshot = migratedSnapshot;
loadedFromLegacy = true;
}
else
{
loadedSnapshot = new LauncherSettingsSnapshot();
}
var normalizedSnapshot = NormalizeSnapshot(loadedSnapshot);
if (loadedFromLegacy)
{
writeTimeUtc = PersistSnapshotToDisk(normalizedSnapshot);
}
UpdateCache(normalizedSnapshot, writeTimeUtc, nowUtc);
return normalizedSnapshot.Clone();
}
}
catch
{
return new LauncherSettingsSnapshot();
}
}
public void Save(LauncherSettingsSnapshot snapshot)
{
var snapshotToPersist = NormalizeSnapshot(snapshot);
try
{
var writeTimeUtc = PersistSnapshotToDisk(snapshotToPersist);
lock (CacheGate)
{
UpdateCache(snapshotToPersist, writeTimeUtc, DateTime.UtcNow);
}
SettingsSaved?.Invoke(InstanceId);
}
catch
{
// Swallow persistence errors to keep UI interactions uninterrupted.
}
}
private bool TryGetCachedWithoutProbe(DateTime nowUtc, out LauncherSettingsSnapshot 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 LauncherSettingsSnapshot snapshot)
{
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) &&
_cachedSnapshot is not null &&
writeTimeUtc == _cachedWriteTimeUtc)
{
snapshot = _cachedSnapshot.Clone();
return true;
}
snapshot = null!;
return false;
}
private LauncherSettingsSnapshot LoadSnapshotFromDisk()
{
try
{
var json = File.ReadAllText(_settingsPath);
var snapshot = JsonSerializer.Deserialize<LauncherSettingsSnapshot>(json, SerializerOptions);
return NormalizeSnapshot(snapshot);
}
catch
{
return new LauncherSettingsSnapshot();
}
}
private bool TryLoadLegacySnapshot(out LauncherSettingsSnapshot snapshot)
{
snapshot = new LauncherSettingsSnapshot();
try
{
if (!File.Exists(_legacyAppSettingsPath))
{
return false;
}
var legacyJson = File.ReadAllText(_legacyAppSettingsPath);
var legacy = JsonSerializer.Deserialize<LegacyLauncherSettingsSnapshot>(legacyJson, SerializerOptions);
if (legacy is null)
{
return false;
}
snapshot = new LauncherSettingsSnapshot
{
HiddenLauncherFolderPaths = legacy.HiddenLauncherFolderPaths ?? [],
HiddenLauncherAppPaths = legacy.HiddenLauncherAppPaths ?? []
};
return true;
}
catch
{
return false;
}
}
private DateTime PersistSnapshotToDisk(LauncherSettingsSnapshot 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 LauncherSettingsSnapshot NormalizeSnapshot(LauncherSettingsSnapshot? snapshot)
{
var normalized = snapshot?.Clone() ?? new LauncherSettingsSnapshot();
normalized.HiddenLauncherFolderPaths = NormalizeKeys(normalized.HiddenLauncherFolderPaths);
normalized.HiddenLauncherAppPaths = NormalizeKeys(normalized.HiddenLauncherAppPaths);
return normalized;
}
private static List<string> NormalizeKeys(IReadOnlyList<string>? values)
{
if (values is null || values.Count == 0)
{
return [];
}
return values
.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(value => value.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private void UpdateCache(LauncherSettingsSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc)
{
_cachedPath = _settingsPath;
_cachedSnapshot = snapshot.Clone();
_cachedWriteTimeUtc = writeTimeUtc;
_lastProbeUtc = probeTimeUtc;
}
private sealed class LegacyLauncherSettingsSnapshot
{
public List<string>? HiddenLauncherFolderPaths { get; set; }
public List<string>? HiddenLauncherAppPaths { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
public enum PluginCatalogSourceKind
{
Package = 0,
Manifest = 1
}
public sealed record PluginCatalogEntry(
PluginManifest Manifest,
string SourcePath,
bool IsPackage,
bool IsEnabled,
bool IsLoaded,
string? ErrorMessage,
int SettingsPageCount,
int WidgetCount);

View File

@@ -0,0 +1,11 @@
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
public sealed record PluginSettingsPageContribution(
LoadedPlugin Plugin,
PluginSettingsPageRegistration Registration);
public sealed record PluginDesktopComponentContribution(
LoadedPlugin Plugin,
PluginDesktopComponentRegistration Registration);

View File

@@ -0,0 +1,315 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
public sealed class PluginRuntimeService : IDisposable
{
private readonly PluginLoader _loader;
private readonly AppSettingsService _appSettingsService = new();
private readonly List<LoadedPlugin> _loadedPlugins = [];
private readonly List<PluginLoadResult> _loadResults = [];
private readonly List<PluginCatalogEntry> _catalog = [];
private readonly List<PluginSettingsPageContribution> _settingsPages = [];
private readonly List<PluginDesktopComponentContribution> _desktopComponents = [];
public PluginRuntimeService()
{
PluginsDirectory = Path.Combine(AppContext.BaseDirectory, "Extensions", "Plugins");
_loader = new PluginLoader(CreateOptions());
}
public string PluginsDirectory { get; }
public IReadOnlyList<LoadedPlugin> LoadedPlugins => _loadedPlugins;
public IReadOnlyList<PluginLoadResult> LoadResults => _loadResults;
public IReadOnlyList<PluginCatalogEntry> Catalog => _catalog;
public IReadOnlyList<PluginSettingsPageContribution> SettingsPages => _settingsPages;
public IReadOnlyList<PluginDesktopComponentContribution> DesktopComponents => _desktopComponents;
public void LoadInstalledPlugins()
{
Directory.CreateDirectory(PluginsDirectory);
UnloadInstalledPlugins();
var disabledPluginIds = GetDisabledPluginIds();
var hostProperties = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
["HostApplicationName"] = "LanMountainDesktop",
["HostVersion"] = typeof(App).Assembly.GetName().Version?.ToString(),
["PluginSdkApiVersion"] = PluginSdkInfo.ApiVersion
};
var discoveryFailures = new List<PluginLoadResult>();
var candidates = DiscoverCandidates(discoveryFailures);
_loadResults.AddRange(discoveryFailures);
var selectedPluginIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var candidate in candidates)
{
if (!selectedPluginIds.Add(candidate.Manifest.Id))
{
var duplicateFailure = PluginLoadResult.Failure(
candidate.SourcePath,
candidate.Manifest,
new InvalidOperationException(
$"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected."));
_loadResults.Add(duplicateFailure);
continue;
}
var isEnabled = !disabledPluginIds.Contains(candidate.Manifest.Id);
if (!isEnabled)
{
_catalog.Add(new PluginCatalogEntry(
candidate.Manifest,
candidate.SourcePath,
candidate.SourceKind == PluginCatalogSourceKind.Package,
false,
false,
null,
0,
0));
continue;
}
var loadResult = candidate.SourceKind switch
{
PluginCatalogSourceKind.Package => _loader.LoadFromPackage(
candidate.SourcePath,
PluginsDirectory,
services: null,
hostProperties),
_ => _loader.LoadFromManifest(
candidate.SourcePath,
services: null,
hostProperties)
};
_loadResults.Add(loadResult);
if (loadResult.IsSuccess && loadResult.LoadedPlugin is not null)
{
_loadedPlugins.Add(loadResult.LoadedPlugin);
CollectContributions(loadResult.LoadedPlugin);
_catalog.Add(new PluginCatalogEntry(
loadResult.LoadedPlugin.Manifest,
loadResult.SourcePath,
candidate.SourceKind == PluginCatalogSourceKind.Package,
true,
true,
null,
loadResult.LoadedPlugin.SettingsPages.Count,
loadResult.LoadedPlugin.DesktopComponents.Count));
Debug.WriteLine($"[PluginRuntime] Loaded '{loadResult.Manifest?.Id}' from '{loadResult.SourcePath}'.");
continue;
}
_catalog.Add(new PluginCatalogEntry(
candidate.Manifest,
candidate.SourcePath,
candidate.SourceKind == PluginCatalogSourceKind.Package,
true,
false,
loadResult.Error?.Message,
0,
0));
Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}");
}
if (_catalog.Count == 0 && discoveryFailures.Count == 0)
{
Debug.WriteLine($"[PluginRuntime] No .laapp packages or loose plugin manifests found under '{PluginsDirectory}'.");
}
}
public bool SetPluginEnabled(string pluginId, bool isEnabled)
{
if (string.IsNullOrWhiteSpace(pluginId))
{
return false;
}
var snapshot = _appSettingsService.Load();
var disabledPluginIds = snapshot.DisabledPluginIds is { Count: > 0 }
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var changed = isEnabled
? disabledPluginIds.Remove(pluginId)
: disabledPluginIds.Add(pluginId);
if (!changed)
{
return false;
}
snapshot.DisabledPluginIds = disabledPluginIds
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
.ToList();
_appSettingsService.Save(snapshot);
for (var i = 0; i < _catalog.Count; i++)
{
if (string.Equals(_catalog[i].Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
{
_catalog[i] = _catalog[i] with { IsEnabled = isEnabled };
}
}
return true;
}
public void Dispose()
{
UnloadInstalledPlugins();
}
private void UnloadInstalledPlugins()
{
for (var i = _loadedPlugins.Count - 1; i >= 0; i--)
{
_loadedPlugins[i].Dispose();
}
_loadedPlugins.Clear();
_loadResults.Clear();
_catalog.Clear();
_settingsPages.Clear();
_desktopComponents.Clear();
}
private HashSet<string> GetDisabledPluginIds()
{
var snapshot = _appSettingsService.Load();
return snapshot.DisabledPluginIds is { Count: > 0 }
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
private IReadOnlyList<PluginCandidate> DiscoverCandidates(List<PluginLoadResult> failures)
{
var candidates = new List<PluginCandidate>();
foreach (var packagePath in EnumerateCandidatePaths($"*{PluginSdkInfo.PackageFileExtension}"))
{
try
{
var manifest = ReadManifestFromPackage(packagePath);
candidates.Add(new PluginCandidate(packagePath, manifest, PluginCatalogSourceKind.Package));
}
catch (Exception ex)
{
failures.Add(PluginLoadResult.Failure(packagePath, null, ex));
}
}
foreach (var manifestPath in EnumerateCandidatePaths("plugin.json"))
{
try
{
var manifest = PluginManifest.Load(manifestPath);
candidates.Add(new PluginCandidate(manifestPath, manifest, PluginCatalogSourceKind.Manifest));
}
catch (Exception ex)
{
failures.Add(PluginLoadResult.Failure(manifestPath, null, ex));
}
}
return candidates
.OrderBy(candidate => candidate.SourceKind)
.ThenBy(candidate => candidate.SourcePath, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private IEnumerable<string> EnumerateCandidatePaths(string searchPattern)
{
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(PluginsDirectory), ".runtime"));
return Directory
.EnumerateFiles(PluginsDirectory, searchPattern, SearchOption.AllDirectories)
.Select(Path.GetFullPath)
.Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase))
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
}
private static PluginManifest ReadManifestFromPackage(string packagePath)
{
using var archive = ZipFile.OpenRead(packagePath);
var entries = archive.Entries
.Where(entry => string.Equals(entry.Name, "plugin.json", StringComparison.OrdinalIgnoreCase))
.ToArray();
if (entries.Length == 0)
{
throw new InvalidOperationException($"Plugin package '{packagePath}' does not contain 'plugin.json'.");
}
if (entries.Length > 1)
{
throw new InvalidOperationException($"Plugin package '{packagePath}' contains multiple 'plugin.json' files.");
}
using var stream = entries[0].Open();
return PluginManifest.Load(stream, $"{packagePath}!/{entries[0].FullName}");
}
private static string EnsureTrailingSeparator(string path)
{
return path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
? path
: path + Path.DirectorySeparatorChar;
}
private static PluginLoaderOptions CreateOptions()
{
var options = new PluginLoaderOptions();
AddSharedAssembly(options, typeof(App).Assembly);
AddSharedAssembly(options, typeof(Application).Assembly);
AddSharedAssembly(options, typeof(Control).Assembly);
AddSharedAssembly(options, typeof(AvaloniaXamlLoader).Assembly);
return options;
}
private static void AddSharedAssembly(PluginLoaderOptions options, Assembly assembly)
{
var assemblyName = assembly.GetName().Name;
if (!string.IsNullOrWhiteSpace(assemblyName))
{
options.SharedAssemblyNames.Add(assemblyName);
}
}
private void CollectContributions(LoadedPlugin loadedPlugin)
{
foreach (var settingsPage in loadedPlugin.SettingsPages)
{
_settingsPages.Add(new PluginSettingsPageContribution(loadedPlugin, settingsPage));
}
foreach (var desktopComponent in loadedPlugin.DesktopComponents)
{
_desktopComponents.Add(new PluginDesktopComponentContribution(loadedPlugin, desktopComponent));
}
}
private sealed record PluginCandidate(
string SourcePath,
PluginManifest Manifest,
PluginCatalogSourceKind SourceKind);
}

View File

@@ -7,11 +7,12 @@ using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IReadOnlyDictionary<string, string> ZhCityNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
@@ -54,9 +55,11 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
private const double DialSize = 258;
private const double Center = DialSize / 2;
private string _componentId = BuiltInComponentIds.DesktopClock;
private string _placementId = string.Empty;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private TimeZoneService? _timeZoneService;
private double _currentCellSize = 48;
@@ -112,6 +115,21 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
UpdateClock();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopClock
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshFromSettings();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
InitializeDialIfNeeded();
@@ -359,7 +377,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
private void LoadClockSettings()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var configuredTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId)

View File

@@ -4,11 +4,12 @@ using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class AnalogClockWidgetSettingsWindow : UserControl
public partial class AnalogClockWidgetSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IReadOnlyDictionary<string, string> ZhTimeZoneNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
@@ -28,11 +29,13 @@ public partial class AnalogClockWidgetSettingsWindow : UserControl
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly TimeZoneService _timeZoneService = new();
private bool _suppressEvents;
private string _languageCode = "zh-CN";
private string _componentId = BuiltInComponentIds.DesktopClock;
private string _placementId = string.Empty;
private IReadOnlyList<TimeZoneInfo> _allTimeZones = Array.Empty<TimeZoneInfo>();
private string _selectedTimeZoneId = string.Empty;
private string _secondHandMode = ClockSecondHandMode.Tick;
@@ -47,10 +50,29 @@ public partial class AnalogClockWidgetSettingsWindow : UserControl
PopulateTimeZoneComboBox();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopClock
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
LoadState();
ApplyLocalization();
PopulateTimeZoneComboBox();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
LoadState();
ApplyLocalization();
PopulateTimeZoneComboBox();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
_selectedTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId)
? "China Standard Time"
@@ -149,10 +171,10 @@ public partial class AnalogClockWidgetSettingsWindow : UserControl
_selectedTimeZoneId = normalizedId;
_secondHandMode = GetSelectedSecondHandMode();
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.DesktopClockTimeZoneId = normalizedId;
snapshot.DesktopClockSecondHandMode = _secondHandMode;
_componentSettingsService.Save(snapshot);
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}

View File

@@ -10,19 +10,22 @@ using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class ClassScheduleSettingsWindow : UserControl
public partial class ClassScheduleSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly List<ImportedClassScheduleSnapshot> _importedSchedules = [];
private string _activeScheduleId = string.Empty;
private string _languageCode = "zh-CN";
private string _componentId = BuiltInComponentIds.DesktopClassSchedule;
private string _placementId = string.Empty;
public event EventHandler? SettingsChanged;
@@ -34,10 +37,29 @@ public partial class ClassScheduleSettingsWindow : UserControl
RenderImportedSchedules();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopClassSchedule
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
LoadState();
ApplyLocalization();
RenderImportedSchedules();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
LoadState();
ApplyLocalization();
RenderImportedSchedules();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
_importedSchedules.Clear();
@@ -299,7 +321,7 @@ public partial class ClassScheduleSettingsWindow : UserControl
private void SaveState()
{
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.ImportedClassSchedules = _importedSchedules
.Select(item => new ImportedClassScheduleSnapshot
{
@@ -309,7 +331,7 @@ public partial class ClassScheduleSettingsWindow : UserControl
})
.ToList();
snapshot.ActiveImportedClassScheduleId = _activeScheduleId ?? string.Empty;
_componentSettingsService.Save(snapshot);
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}

View File

@@ -7,12 +7,13 @@ using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private sealed record CourseItemViewModel(
string Name,
@@ -26,7 +27,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
@@ -35,6 +36,8 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
private IReadOnlyList<CourseItemViewModel> _courseItems = Array.Empty<CourseItemViewModel>();
private bool _isNightVisual = true;
private string _languageCode = "zh-CN";
private string _componentId = BuiltInComponentIds.DesktopClassSchedule;
private string _placementId = string.Empty;
public ClassScheduleWidget()
{
@@ -113,10 +116,25 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
RefreshSchedule();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopClassSchedule
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshSchedule();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshSchedule();
}
private void RefreshSchedule()
{
var appSettings = _appSettingsService.Load();
var componentSettings = _componentSettingsService.Load();
var componentSettings = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
UpdateHeader(now);

View File

@@ -2,18 +2,21 @@ using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class DailyArtworkSettingsWindow : UserControl
public partial class DailyArtworkSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private string _languageCode = "zh-CN";
private bool _suppressEvents;
private string _componentId = BuiltInComponentIds.DesktopDailyArtwork;
private string _placementId = string.Empty;
public event EventHandler? SettingsChanged;
@@ -24,10 +27,27 @@ public partial class DailyArtworkSettingsWindow : UserControl
ApplyLocalization();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopDailyArtwork
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
LoadState();
ApplyLocalization();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var source = DailyArtworkMirrorSources.Normalize(componentSnapshot.DailyArtworkMirrorSource);
@@ -59,9 +79,9 @@ public partial class DailyArtworkSettingsWindow : UserControl
}
var source = GetSelectedSource();
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.DailyArtworkMirrorSource = source;
_componentSettingsService.Save(snapshot);
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
UpdateSourceStatus(source);
SettingsChanged?.Invoke(this, EventArgs.Empty);

View File

@@ -13,12 +13,13 @@ using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, IRecommendationInfoAwareComponentWidget
public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, IRecommendationInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IReadOnlyDictionary<DayOfWeek, string> ZhWeekdays =
new Dictionary<DayOfWeek, string>
@@ -58,6 +59,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
};
private readonly AppSettingsService _settingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
@@ -67,6 +69,8 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
private double _currentCellSize = BaseCellSize;
private bool _isAttached;
private bool _isRefreshing;
private string _componentId = BuiltInComponentIds.DesktopDailyArtwork;
private string _placementId = string.Empty;
private string? _currentArtworkSourceUrl;
private string? _currentArtworkImageUrl;
@@ -136,6 +140,21 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
}
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopDailyArtwork
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshFromSettings();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
@@ -203,6 +222,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
{
var query = new DailyArtworkQuery(
Locale: _languageCode,
MirrorSource: ResolveMirrorSource(),
ForceRefresh: forceRefresh);
var result = await _recommendationService.GetDailyArtworkAsync(query, cts.Token);
if (!_isAttached || cts.IsCancellationRequested)
@@ -640,6 +660,19 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
}
}
private string ResolveMirrorSource()
{
try
{
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
return DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
}
catch
{
return DailyArtworkMirrorSources.Overseas;
}
}
private void UpdateDateLabels()
{
var now = DateTime.Now;

View File

@@ -7,24 +7,65 @@ using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public sealed record DesktopComponentRuntimeRegistration(
string ComponentId,
string DisplayNameLocalizationKey,
Func<Control> ControlFactory,
Func<double, double>? CornerRadiusResolver = null);
public sealed record DesktopComponentControlFactoryContext(
DesktopComponentDefinition Definition,
double CellSize,
TimeZoneService TimeZoneService,
IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService,
ICalculatorDataService CalculatorDataService,
IComponentInstanceSettingsStore ComponentSettingsStore,
string? PlacementId = null);
public sealed class DesktopComponentRuntimeRegistration
{
public DesktopComponentRuntimeRegistration(
string componentId,
string? displayNameLocalizationKey,
Func<Control> controlFactory,
Func<double, double>? cornerRadiusResolver = null)
: this(componentId, displayNameLocalizationKey, _ => controlFactory(), cornerRadiusResolver)
{
}
public DesktopComponentRuntimeRegistration(
string componentId,
string? displayNameLocalizationKey,
Func<DesktopComponentControlFactoryContext, Control> controlFactory,
Func<double, double>? cornerRadiusResolver = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
ArgumentNullException.ThrowIfNull(controlFactory);
ComponentId = componentId.Trim();
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(displayNameLocalizationKey)
? null
: displayNameLocalizationKey.Trim();
ControlFactory = controlFactory;
CornerRadiusResolver = cornerRadiusResolver;
}
public string ComponentId { get; }
public string? DisplayNameLocalizationKey { get; }
public Func<DesktopComponentControlFactoryContext, Control> ControlFactory { get; }
public Func<double, double>? CornerRadiusResolver { get; }
}
public sealed class DesktopComponentRuntimeDescriptor
{
private static readonly Func<double, double> DefaultCornerRadiusResolver =
cellSize => Math.Clamp(cellSize * 0.22, 8, 18);
private readonly Func<Control> _controlFactory;
private readonly Func<DesktopComponentControlFactoryContext, Control> _controlFactory;
private readonly Func<double, double> _cornerRadiusResolver;
internal DesktopComponentRuntimeDescriptor(
DesktopComponentDefinition definition,
string displayNameLocalizationKey,
Func<Control> controlFactory,
string? displayNameLocalizationKey,
Func<DesktopComponentControlFactoryContext, Control> controlFactory,
Func<double, double>? cornerRadiusResolver)
{
Definition = definition;
@@ -35,16 +76,48 @@ public sealed class DesktopComponentRuntimeDescriptor
public DesktopComponentDefinition Definition { get; }
public string DisplayNameLocalizationKey { get; }
public string? DisplayNameLocalizationKey { get; }
public Control CreateControl(
double cellSize,
TimeZoneService timeZoneService,
IWeatherInfoService weatherInfoService,
IRecommendationInfoService recommendationInfoService,
ICalculatorDataService calculatorDataService)
ICalculatorDataService calculatorDataService,
IComponentInstanceSettingsStore componentSettingsStore,
string? placementId = null)
{
var control = _controlFactory();
var control = _controlFactory(new DesktopComponentControlFactoryContext(
Definition,
cellSize,
timeZoneService,
weatherInfoService,
recommendationInfoService,
calculatorDataService,
componentSettingsStore,
placementId));
var runtimeContext = new DesktopComponentRuntimeContext(
Definition.Id,
placementId,
componentSettingsStore);
if (control is IComponentRuntimeContextAware runtimeContextAwareComponent)
{
runtimeContextAwareComponent.SetComponentRuntimeContext(runtimeContext);
}
if (control is IComponentPlacementContextAware placementAwareComponent)
{
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);
@@ -109,12 +182,10 @@ public sealed class DesktopComponentRuntimeRegistry
StringComparer.OrdinalIgnoreCase);
}
public static DesktopComponentRuntimeRegistry CreateDefault(ComponentRegistry componentRegistry)
public static IReadOnlyList<DesktopComponentRuntimeRegistration> GetDefaultRegistrations()
{
return new DesktopComponentRuntimeRegistry(
componentRegistry,
new[]
{
return
[
new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.Date,
"component.date",
@@ -295,7 +366,12 @@ public sealed class DesktopComponentRuntimeRegistry
"component.holiday_calendar",
() => new HolidayCalendarWidget(),
cellSize => Math.Clamp(cellSize * 0.32, 12, 28))
});
];
}
public static DesktopComponentRuntimeRegistry CreateDefault(ComponentRegistry componentRegistry)
{
return new DesktopComponentRuntimeRegistry(componentRegistry, GetDefaultRegistrations());
}
public bool TryGetDescriptor(string componentId, out DesktopComponentRuntimeDescriptor descriptor)

View File

@@ -9,13 +9,14 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components;
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
@@ -25,7 +26,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;
@@ -39,6 +40,8 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private bool _autoRefreshEnabled = true;
private string _languageCode = "zh-CN";
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.ClearDay;
private string _componentId = BuiltInComponentIds.DesktopExtendedWeather;
private string _placementId = string.Empty;
private readonly TextBlock[] _hourlyTempBlocks;
private readonly TextBlock[] _hourlyTimeBlocks;
private readonly Image[] _hourlyIconBlocks;
@@ -173,6 +176,21 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
}
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopExtendedWeather
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshFromSettings();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_ = isEditMode;
@@ -935,7 +953,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
try
{
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
enabled = snapshot.WeatherAutoRefreshEnabled;
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
}

View File

@@ -11,13 +11,14 @@ using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components;
public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private enum WeatherVisualKind
{
@@ -95,7 +96,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
};
private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
@@ -118,6 +119,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private bool _isOnActivePage = true;
private bool _isRefreshing;
private bool _autoRefreshEnabled = true;
private string _componentId = BuiltInComponentIds.DesktopHourlyWeather;
private string _placementId = string.Empty;
private readonly TextBlock[] _hourlyTimeBlocks;
private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _hourlyTempBlocks;
@@ -224,6 +227,21 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
}
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopHourlyWeather
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshFromSettings();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_ = isEditMode;
@@ -1424,7 +1442,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
try
{
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
enabled = snapshot.WeatherAutoRefreshEnabled;
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
}

View File

@@ -9,13 +9,14 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components;
public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private enum WeatherVisualKind
{
@@ -93,7 +94,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
};
private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
@@ -116,6 +117,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private bool _isOnActivePage = true;
private bool _isRefreshing;
private bool _autoRefreshEnabled = true;
private string _componentId = BuiltInComponentIds.DesktopMultiDayWeather;
private string _placementId = string.Empty;
private readonly TextBlock[] _hourlyTimeBlocks;
private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _hourlyTempBlocks;
@@ -222,6 +225,21 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
}
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopMultiDayWeather
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshFromSettings();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_ = isEditMode;
@@ -1274,7 +1292,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
try
{
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
enabled = snapshot.WeatherAutoRefreshEnabled;
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
}

View File

@@ -10,12 +10,13 @@ using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private sealed record WeatherClockConfig(
string LanguageCode,
@@ -41,7 +42,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
};
private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly Line _hourHandLine = CreateHandLine("#232938", 4.0);
private readonly Line _minuteHandLine = CreateHandLine("#2F3749", 2.8);
@@ -59,6 +60,8 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
private bool? _isNightModeApplied;
private string _languageCode = "zh-CN";
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.CloudyDay;
private string _componentId = BuiltInComponentIds.DesktopWeatherClock;
private string _placementId = string.Empty;
public WeatherClockWidget()
{
@@ -116,6 +119,21 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
}
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopWeatherClock
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshFromSettings();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
@@ -659,7 +677,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
try
{
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
enabled = snapshot.WeatherAutoRefreshEnabled;
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
}

View File

@@ -11,13 +11,14 @@ using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components;
public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private enum WeatherVisualKind
{
@@ -89,7 +90,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
};
private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
@@ -112,6 +113,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private bool _isOnActivePage = true;
private bool _isRefreshing;
private bool _autoRefreshEnabled = true;
private string _componentId = BuiltInComponentIds.DesktopWeather;
private string _placementId = string.Empty;
public WeatherWidget()
{
@@ -167,6 +170,21 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
}
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopWeather
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshFromSettings();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_ = isEditMode;
@@ -1063,7 +1081,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
try
{
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
enabled = snapshot.WeatherAutoRefreshEnabled;
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
}

View File

@@ -4,20 +4,23 @@ using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class WeatherWidgetSettingsWindow : UserControl
public partial class WeatherWidgetSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private bool _suppressEvents;
private string _languageCode = "zh-CN";
private string _componentId = BuiltInComponentIds.DesktopWeather;
private string _placementId = string.Empty;
public event EventHandler? SettingsChanged;
@@ -29,10 +32,27 @@ public partial class WeatherWidgetSettingsWindow : UserControl
ApplyLocalization();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopWeather
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
LoadState();
ApplyLocalization();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var enabled = componentSnapshot.WeatherAutoRefreshEnabled;
@@ -83,10 +103,10 @@ public partial class WeatherWidgetSettingsWindow : UserControl
private void SaveState()
{
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.WeatherAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
snapshot.WeatherAutoRefreshIntervalMinutes = GetSelectedInterval();
_componentSettingsService.Save(snapshot);
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}

View File

@@ -8,11 +8,12 @@ using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget
public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private const int BaseWidthCells = 4;
private const int BaseHeightCells = 2;
@@ -92,7 +93,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly ClockEntryVisual[] _entryVisuals = new ClockEntryVisual[WorldClockTimeZoneCatalog.ClockCount];
private readonly TimeZoneInfo[] _entryTimeZones = new TimeZoneInfo[WorldClockTimeZoneCatalog.ClockCount];
@@ -103,6 +104,8 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
private DateTime _nextLanguageProbeUtc = DateTime.MinValue;
private string _secondHandMode = ClockSecondHandMode.Tick;
private bool _isNightVisual = true;
private string _componentId = BuiltInComponentIds.DesktopWorldClock;
private string _placementId = string.Empty;
public WorldClockWidget()
{
@@ -147,6 +150,21 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
UpdateClockVisuals();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopWorldClock
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
RefreshFromSettings();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
RefreshFromSettings();
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
@@ -525,7 +543,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
private void LoadFromSettings()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var ids = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(componentSnapshot.WorldClockTimeZoneIds);

View File

@@ -4,11 +4,12 @@ using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class WorldClockWidgetSettingsWindow : UserControl
public partial class WorldClockWidgetSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IReadOnlyDictionary<string, string> ZhTimeZoneNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
@@ -28,12 +29,14 @@ public partial class WorldClockWidgetSettingsWindow : UserControl
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly LocalizationService _localizationService = new();
private readonly TimeZoneService _timeZoneService = new();
private readonly ComboBox[] _timeZoneComboBoxes;
private bool _suppressEvents;
private string _languageCode = "zh-CN";
private string _componentId = BuiltInComponentIds.DesktopWorldClock;
private string _placementId = string.Empty;
private IReadOnlyList<TimeZoneInfo> _allTimeZones = Array.Empty<TimeZoneInfo>();
private IReadOnlyList<string> _selectedTimeZoneIds = Array.Empty<string>();
private string _secondHandMode = ClockSecondHandMode.Tick;
@@ -57,10 +60,29 @@ public partial class WorldClockWidgetSettingsWindow : UserControl
PopulateTimeZoneComboBoxes();
}
public void SetComponentPlacementContext(string componentId, string? placementId)
{
_componentId = string.IsNullOrWhiteSpace(componentId)
? BuiltInComponentIds.DesktopWorldClock
: componentId.Trim();
_placementId = placementId?.Trim() ?? string.Empty;
LoadState();
ApplyLocalization();
PopulateTimeZoneComboBoxes();
}
public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore)
{
_componentSettingsStore = settingsStore ?? new ComponentSettingsService();
LoadState();
ApplyLocalization();
PopulateTimeZoneComboBoxes();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
_allTimeZones = _timeZoneService
@@ -167,10 +189,10 @@ public partial class WorldClockWidgetSettingsWindow : UserControl
var normalizedIds = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(selectedIds, _allTimeZones);
_secondHandMode = GetSelectedSecondHandMode();
var snapshot = _componentSettingsService.Load();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.WorldClockTimeZoneIds = normalizedIds.ToList();
snapshot.WorldClockSecondHandMode = _secondHandMode;
_componentSettingsService.Save(snapshot);
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
_selectedTimeZoneIds = normalizedIds;
SettingsChanged?.Invoke(this, EventArgs.Empty);

View File

@@ -14,6 +14,7 @@ using FluentIcons.Avalonia;
using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components;
@@ -255,14 +256,15 @@ public partial class MainWindow
return TaskbarContext.Desktop;
}
return SettingsNavListBox?.SelectedIndex switch
var selectedItem = SettingsNavView?.SelectedItem as FluentAvalonia.UI.Controls.NavigationViewItem;
return selectedItem?.Tag?.ToString() switch
{
0 => TaskbarContext.SettingsWallpaper,
1 => TaskbarContext.SettingsGrid,
2 => TaskbarContext.SettingsColor,
3 => TaskbarContext.SettingsStatusBar,
4 => TaskbarContext.SettingsWeather,
5 => TaskbarContext.SettingsRegion,
"Wallpaper" => TaskbarContext.SettingsWallpaper,
"Grid" => TaskbarContext.SettingsGrid,
"Color" => TaskbarContext.SettingsColor,
"StatusBar" => TaskbarContext.SettingsStatusBar,
"Weather" => TaskbarContext.SettingsWeather,
"Region" => TaskbarContext.SettingsRegion,
_ => TaskbarContext.Desktop
};
}
@@ -689,6 +691,7 @@ public partial class MainWindow
}
_desktopComponentPlacements.Remove(placement);
_componentSettingsService.DeleteForComponent(placement.ComponentId, placement.PlacementId);
ClearDesktopComponentSelection();
@@ -724,80 +727,80 @@ public partial class MainWindow
if (placement.ComponentId == BuiltInComponentIds.Date)
{
OpenDateComponentSettings();
OpenDateComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopClock)
{
OpenDesktopClockComponentSettings();
OpenDesktopClockComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopClassSchedule)
{
OpenClassScheduleComponentSettings();
OpenClassScheduleComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopWorldClock)
{
OpenWorldClockComponentSettings();
OpenWorldClockComponentSettings(placement);
return;
}
if (IsWeatherComponentId(placement.ComponentId))
{
OpenWeatherComponentSettings();
OpenWeatherComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork)
{
OpenDailyArtworkComponentSettings();
OpenDailyArtworkComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopCnrDailyNews)
{
OpenCnrDailyNewsComponentSettings();
OpenCnrDailyNewsComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopIfengNews)
{
OpenIfengNewsComponentSettings();
OpenIfengNewsComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyWord ||
placement.ComponentId == BuiltInComponentIds.DesktopDailyWord2x2)
{
OpenDailyWordComponentSettings();
OpenDailyWordComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopBilibiliHotSearch)
{
OpenBilibiliHotSearchComponentSettings();
OpenBilibiliHotSearchComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopBaiduHotSearch)
{
OpenBaiduHotSearchComponentSettings();
OpenBaiduHotSearchComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopStcn24Forum)
{
OpenStcn24ForumComponentSettings();
OpenStcn24ForumComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment)
{
OpenStudyEnvironmentComponentSettings();
OpenStudyEnvironmentComponentSettings(placement);
return;
}
}
@@ -811,212 +814,129 @@ public partial class MainWindow
string.Equals(componentId, BuiltInComponentIds.DesktopExtendedWeather, StringComparison.OrdinalIgnoreCase);
}
private void OpenDateComponentSettings()
private void ShowComponentSettings(Control settingsContent, DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new DateWidgetSettingsWindow();
var runtimeContext = new DesktopComponentRuntimeContext(
placement.ComponentId,
placement.PlacementId,
_componentSettingsService);
if (settingsContent is IComponentRuntimeContextAware runtimeContextAwareComponent)
{
runtimeContextAwareComponent.SetComponentRuntimeContext(runtimeContext);
}
if (settingsContent is IComponentPlacementContextAware placementAwareComponent)
{
placementAwareComponent.SetComponentPlacementContext(placement.ComponentId, placement.PlacementId);
}
if (settingsContent is IComponentSettingsStoreAware settingsStoreAwareComponent)
{
settingsStoreAwareComponent.SetComponentSettingsStore(_componentSettingsService);
}
ComponentSettingsService.ApplyScopedContextToTarget(settingsContent, placement.ComponentId, placement.PlacementId);
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
}
private void OpenClassScheduleComponentSettings()
private void OpenDateComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new DateWidgetSettingsWindow();
ShowComponentSettings(settingsContent, placement);
}
private void OpenClassScheduleComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new ClassScheduleSettingsWindow();
settingsContent.SettingsChanged += OnClassScheduleSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenDesktopClockComponentSettings()
private void OpenDesktopClockComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new AnalogClockWidgetSettingsWindow();
settingsContent.SettingsChanged += OnDesktopClockSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenWorldClockComponentSettings()
private void OpenWorldClockComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new WorldClockWidgetSettingsWindow();
settingsContent.SettingsChanged += OnWorldClockSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenWeatherComponentSettings()
private void OpenWeatherComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new WeatherWidgetSettingsWindow();
settingsContent.SettingsChanged += OnWeatherSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenStudyEnvironmentComponentSettings()
private void OpenStudyEnvironmentComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new StudyEnvironmentWidgetSettingsWindow();
settingsContent.SettingsChanged += OnStudyEnvironmentSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenDailyArtworkComponentSettings()
private void OpenDailyArtworkComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new DailyArtworkSettingsWindow();
settingsContent.SettingsChanged += OnDailyArtworkSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenCnrDailyNewsComponentSettings()
private void OpenCnrDailyNewsComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new CnrDailyNewsSettingsWindow();
settingsContent.SettingsChanged += OnCnrDailyNewsSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenIfengNewsComponentSettings()
private void OpenIfengNewsComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new IfengNewsSettingsWindow();
settingsContent.SettingsChanged += OnIfengNewsSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenDailyWordComponentSettings()
private void OpenDailyWordComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new DailyWordSettingsWindow();
settingsContent.SettingsChanged += OnDailyWordSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenBilibiliHotSearchComponentSettings()
private void OpenBilibiliHotSearchComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new BilibiliHotSearchSettingsWindow();
settingsContent.SettingsChanged += OnBilibiliHotSearchSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenBaiduHotSearchComponentSettings()
private void OpenBaiduHotSearchComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new BaiduHotSearchSettingsWindow();
settingsContent.SettingsChanged += OnBaiduHotSearchSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OpenStcn24ForumComponentSettings()
private void OpenStcn24ForumComponentSettings(DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new Stcn24ForumSettingsWindow();
settingsContent.SettingsChanged += OnStcn24ForumSettingsChanged;
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
ShowComponentSettings(settingsContent, placement);
}
private void OnClassScheduleSettingsChanged(object? sender, EventArgs e)
@@ -1408,6 +1328,7 @@ public partial class MainWindow
foreach (var placement in placementsToRemove)
{
_desktopComponentPlacements.Remove(placement);
_componentSettingsService.DeleteForComponent(placement.ComponentId, placement.PlacementId);
}
_desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount);
@@ -1431,7 +1352,7 @@ public partial class MainWindow
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
private void InitializeDesktopComponentPlacements(AppSettingsSnapshot snapshot)
private void InitializeDesktopComponentPlacements(DesktopLayoutSettingsSnapshot snapshot)
{
_desktopComponentPlacements.Clear();
@@ -1601,7 +1522,7 @@ public partial class MainWindow
placement.PlacementId = Guid.NewGuid().ToString("N");
}
var component = CreateDesktopComponentControl(placement.ComponentId);
var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId);
if (component is null)
{
return null;
@@ -2035,7 +1956,7 @@ public partial class MainWindow
return onLeft || onRight || onTop || onBottom;
}
private Control? CreateDesktopComponentControl(string componentId)
private Control? CreateDesktopComponentControl(string componentId, string? placementId = null)
{
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
{
@@ -2047,7 +1968,9 @@ public partial class MainWindow
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService);
_calculatorDataService,
_componentSettingsService,
placementId);
component.Classes.Add(DesktopComponentClass);
return component;
}
@@ -3195,7 +3118,8 @@ public partial class MainWindow
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService);
_calculatorDataService,
_componentSettingsService);
// Component library previews must stay non-interactive so drag gesture is reliable.
previewControl.IsHitTestVisible = false;
previewControl.Focusable = false;
@@ -3292,7 +3216,9 @@ public partial class MainWindow
private string GetLocalizedComponentDisplayName(DesktopComponentRuntimeDescriptor descriptor)
{
return L(descriptor.DisplayNameLocalizationKey, descriptor.Definition.DisplayName);
return string.IsNullOrWhiteSpace(descriptor.DisplayNameLocalizationKey)
? descriptor.Definition.DisplayName
: L(descriptor.DisplayNameLocalizationKey, descriptor.Definition.DisplayName);
}
private void OnComponentLibraryComponentPreviewPointerPressed(object? sender, PointerPressedEventArgs e)
@@ -3598,4 +3524,3 @@ public partial class MainWindow
ApplyComponentLibraryComponentOffset();
}
}

View File

@@ -66,14 +66,14 @@ public partial class MainWindow
private int TotalSurfaceCount => LauncherSurfaceIndex + 1;
private void InitializeDesktopSurfaceState(AppSettingsSnapshot snapshot)
private void InitializeDesktopSurfaceState(DesktopLayoutSettingsSnapshot snapshot)
{
var loadedPageCount = snapshot.DesktopPageCount <= 0 ? MinDesktopPageCount : snapshot.DesktopPageCount;
_desktopPageCount = Math.Clamp(loadedPageCount, MinDesktopPageCount, MaxDesktopPageCount);
_currentDesktopSurfaceIndex = Math.Clamp(snapshot.CurrentDesktopSurfaceIndex, 0, LauncherSurfaceIndex);
}
private void InitializeLauncherVisibilitySettings(AppSettingsSnapshot snapshot)
private void InitializeLauncherVisibilitySettings(LauncherSettingsSnapshot snapshot)
{
_hiddenLauncherFolderPaths.Clear();
if (snapshot.HiddenLauncherFolderPaths is not null)

View File

@@ -107,16 +107,16 @@ public partial class MainWindow
ToolTip.SetTip(LauncherFolderBackButton, L("common.back", "Back"));
ToolTip.SetTip(LauncherFolderCloseButton, L("common.close", "Close"));
SettingsNavHeaderTextBlock.Text = L("settings.nav_header", "Settings");
SettingsNavWallpaperTextBlock.Text = L("settings.nav.wallpaper", "Wallpaper");
SettingsNavGridTextBlock.Text = L("settings.nav.grid", "Grid");
SettingsNavColorTextBlock.Text = L("settings.nav.color", "Color");
SettingsNavStatusBarTextBlock.Text = L("settings.nav.status_bar", "Status Bar");
SettingsNavWeatherTextBlock.Text = L("settings.nav.weather", "Weather");
SettingsNavRegionTextBlock.Text = L("settings.nav.region", "Region");
SettingsNavUpdateTextBlock.Text = L("settings.nav.update", "Update");
SettingsNavLauncherTextBlock.Text = L("settings.nav.launcher", "App Launcher");
SettingsNavPluginsTextBlock.Text = L("settings.nav.plugins", "Plugins");
// 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");
WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "Personalize your wallpaper");
WallpaperPlacementSettingsExpander.Header = L("settings.wallpaper.placement_label", "Placement");
@@ -127,10 +127,10 @@ public partial class MainWindow
ClearWallpaperButton.Content = L("settings.wallpaper.clear_button", "Reset");
GridPanelTitleTextBlock.Text = L("settings.grid.title", "Grid Layout");
GridSpacingPresetLabelTextBlock.Text = L("settings.grid.spacing_label", "Grid Spacing");
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");
GridEdgeInsetLabelTextBlock.Text = L("settings.grid.edge_inset_label", "Screen Inset");
GridEdgeInsetSettingsExpander.Header = L("settings.grid.edge_inset_label", "Screen Inset");
ApplyGridButton.Content = L("settings.grid.apply_button", "Apply");
UpdateGridEdgeInsetComputedPxText(_currentDesktopCellSize);
@@ -175,7 +175,7 @@ public partial class MainWindow
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");
StatusBarSpacingCustomLabelTextBlock.Text = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
StatusBarSpacingCustomPanel.Content = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
WeatherPanelTitleTextBlock.Text = L("settings.weather.title", "Weather");
WeatherLocationSettingsExpander.Header = L("settings.weather.location_source_header", "Location Source");
@@ -212,6 +212,14 @@ public partial class MainWindow
"Refresh and verify current weather service status.");
WeatherPreviewButton.Content = L("settings.weather.refresh_button", "Refresh");
WeatherLocationSettingsExpander.Header = L("settings.weather.location_msg_header", "Location Source");
WeatherLocationSettingsExpander.Description = L(
"settings.weather.location_msg_desc",
"Choose how weather widgets resolve location.");
WeatherLocationModeCityChipItem.Content = L("settings.weather.mode_city", "City Search");
WeatherLocationModeCoordinatesChipItem.Content = L("settings.weather.mode_coordinates", "Coordinates");
WeatherAutoRefreshToggleSwitch.Content = L("settings.weather.auto_location_toggle", "Auto refresh location on startup");
WeatherAlertFilterSettingsExpander.Header = L("settings.weather.alert_filter_header", "Excluded Alerts");
WeatherAlertFilterSettingsExpander.Description = L(
"settings.weather.alert_filter_desc",
@@ -248,6 +256,7 @@ public partial class MainWindow
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");
@@ -271,15 +280,24 @@ public partial class MainWindow
PluginSystemSettingsExpander.Header = L("settings.plugins.runtime_header", "Plugin Runtime");
PluginSystemSettingsExpander.Description = L(
"settings.plugins.runtime_desc",
"Manage plugin loading and backend isolation.");
"Review plugin runtime state and load results.");
PluginSystemDescriptionTextBlock.Text = L(
"settings.plugins.runtime_hint",
"This page will host installed plugin management, permission review, and sandboxed backend runtime controls.");
"This page shows discovery status, load results, and runtime diagnostics for installed plugins.");
PluginSystemStatusTextBlock.Text = L(
"settings.plugins.runtime_status",
"Plugin management UI is not connected yet. Next step is wiring the loader, permissions, and worker isolation state into this panel.");
"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",
"Enable or disable plugins here. Detailed plugin settings appear as separate settings pages.");
PluginRestartHintTextBlock.Text = L(
"settings.plugins.restart_hint",
"Plugin enable state changes take effect after restarting the app.");
PluginCatalogEmptyTextBlock.Text = L("settings.plugins.empty", "No plugins found.");
PluginSettingsPanel.RefreshFromRuntime();
SettingsNavAboutTextBlock.Text = L("settings.nav.about", "About");
SettingsNavAboutItem.Content = L("settings.nav.about", "About");
AboutPanelTitleTextBlock.Text = L("settings.about.title", "About");
VersionTextBlock.Text = Lf(
"settings.about.version_format",
@@ -419,3 +437,4 @@ public partial class MainWindow
: _weatherLocationKey);
}
}

View File

@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private readonly Dictionary<string, Control> _pluginSettingsPageHosts = new(StringComparer.OrdinalIgnoreCase);
private void InitializePluginSettingsNavigation()
{
if (_pluginSettingsPageHosts.Count > 0 || SettingsNavView?.MenuItems is null)
{
return;
}
var runtime = (Application.Current as App)?.PluginRuntimeService;
var contributions = runtime?.SettingsPages
.OrderBy(contribution => contribution.Registration.SortOrder)
.ThenBy(contribution => contribution.Plugin.Manifest.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(contribution => contribution.Registration.Title, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (contributions is not { Length: > 0 })
{
return;
}
var pageCountsByPluginId = contributions
.GroupBy(contribution => contribution.Plugin.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
var insertIndex = SettingsNavView.MenuItems.IndexOf(SettingsNavPluginsItem) + 1;
foreach (var contribution in contributions)
{
var tag = BuildPluginSettingsTag(contribution);
var navigationTitle = BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId);
var navItem = new NavigationViewItem
{
Content = navigationTitle,
Tag = tag,
IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = FluentIcons.Common.Symbol.PuzzlePiece,
IconVariant = FluentIcons.Common.IconVariant.Regular
}
};
ToolTip.SetTip(navItem, $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}");
SettingsNavView.MenuItems.Insert(insertIndex++, navItem);
var pageHost = CreatePluginSettingsPageHost(contribution);
pageHost.IsVisible = false;
SettingsContentPagesHost.Children.Add(pageHost);
_pluginSettingsPageHosts[tag] = pageHost;
}
}
private static string BuildPluginSettingsTag(PluginSettingsPageContribution contribution)
{
return $"PluginPage:{contribution.Plugin.Manifest.Id}:{contribution.Registration.Id}";
}
private static string BuildPluginSettingsNavigationTitle(
PluginSettingsPageContribution contribution,
IReadOnlyDictionary<string, int> pageCountsByPluginId)
{
return pageCountsByPluginId.TryGetValue(contribution.Plugin.Manifest.Id, out var pageCount) && pageCount > 1
? $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"
: contribution.Plugin.Manifest.Name;
}
private Control CreatePluginSettingsPageHost(PluginSettingsPageContribution contribution)
{
Control content;
try
{
content = contribution.Registration.ContentFactory();
}
catch (Exception ex)
{
content = CreatePluginPageErrorContent(ex);
}
return new StackPanel
{
Spacing = 16,
Children =
{
new TextBlock
{
Text = contribution.Registration.Title,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush")
},
new TextBlock
{
Text = contribution.Plugin.Manifest.Name,
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush")
},
content
}
};
}
private Control CreatePluginPageErrorContent(Exception exception)
{
return new Border
{
Background = new SolidColorBrush(Color.Parse("#332B0F16")),
BorderBrush = new SolidColorBrush(Color.Parse("#66F97316")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(16),
Child = new TextBlock
{
Text = exception.Message,
TextWrapping = TextWrapping.Wrap
}
};
}
private void UpdatePluginSettingsPageVisibility(string? selectedTag)
{
foreach (var pair in _pluginSettingsPageHosts)
{
pair.Value.IsVisible = string.Equals(pair.Key, selectedTag, StringComparison.OrdinalIgnoreCase);
}
}
private string? GetSelectedSettingsTabTag()
{
return (SettingsNavView?.SelectedItem as NavigationViewItem)?.Tag?.ToString();
}
private int ResolveSelectedSettingsTabIndex()
{
if (SettingsNavView?.SelectedItem is null || SettingsNavView.MenuItems is null)
{
return 0;
}
for (var i = 0; i < SettingsNavView.MenuItems.Count; i++)
{
if (ReferenceEquals(SettingsNavView.MenuItems[i], SettingsNavView.SelectedItem))
{
return i;
}
}
return 0;
}
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag))
{
var taggedItem = SettingsNavView.MenuItems
.OfType<NavigationViewItem>()
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), snapshot.SettingsTabTag, StringComparison.OrdinalIgnoreCase));
if (taggedItem is not null)
{
SettingsNavView.SelectedItem = taggedItem;
return;
}
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, SettingsNavView.MenuItems.Count - 1));
if (SettingsNavView.MenuItems[safeIndex] is NavigationViewItem navItem)
{
SettingsNavView.SelectedItem = navItem;
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using FluentIcons.Avalonia;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using LanMountainDesktop.Views.Components;
@@ -19,6 +20,7 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
@@ -28,6 +30,47 @@ namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private void OnExternalAppSettingsSaved(string sourceInstanceId)
{
if (string.Equals(sourceInstanceId, _appSettingsService.InstanceId, StringComparison.Ordinal))
{
return;
}
ScheduleReloadFromExternalSettings();
}
private void OnExternalLauncherSettingsSaved(string sourceInstanceId)
{
if (string.Equals(sourceInstanceId, _launcherSettingsService.InstanceId, StringComparison.Ordinal))
{
return;
}
ScheduleReloadFromExternalSettings();
}
private void ScheduleReloadFromExternalSettings()
{
if (_externalSettingsReloadPending)
{
return;
}
_externalSettingsReloadPending = true;
Dispatcher.UIThread.Post(() =>
{
_externalSettingsReloadPending = false;
if (!IsVisible)
{
return;
}
ReloadFromPersistedSettings();
}, DispatcherPriority.Background);
}
private void OnOpenSettingsClick(object? sender, RoutedEventArgs e)
{
if (_isComponentLibraryOpen)
@@ -49,16 +92,20 @@ public partial class MainWindow
CloseSettingsPage();
}
private void OnSettingsNavSelectionChanged(object? sender, SelectionChangedEventArgs e)
private void OnSettingsNavSelectionChanged(object? sender, FluentAvalonia.UI.Controls.NavigationViewSelectionChangedEventArgs e)
{
UpdateSettingsTabContent();
PersistSettings();
}
private int GetSettingsTabIndex()
{
return ResolveSelectedSettingsTabIndex();
}
private void UpdateSettingsTabContent()
{
// SelectionChanged can fire during XAML initialization before all named controls are assigned.
if (SettingsNavListBox is null ||
if (SettingsNavView is null ||
GridSettingsPanel is null ||
WallpaperSettingsPanel is null ||
ColorSettingsPanel is null ||
@@ -73,24 +120,27 @@ public partial class MainWindow
return;
}
var selectedIndex = SettingsNavListBox.SelectedIndex;
WallpaperSettingsPanel.IsVisible = selectedIndex == 0;
GridSettingsPanel.IsVisible = selectedIndex == 1;
ColorSettingsPanel.IsVisible = selectedIndex == 2;
StatusBarSettingsPanel.IsVisible = selectedIndex == 3;
WeatherSettingsPanel.IsVisible = selectedIndex == 4;
RegionSettingsPanel.IsVisible = selectedIndex == 5;
UpdateSettingsPanel.IsVisible = selectedIndex == 6;
AboutSettingsPanel.IsVisible = selectedIndex == 7;
LauncherSettingsPanel.IsVisible = selectedIndex == 8;
PluginSettingsPanel.IsVisible = selectedIndex == 9;
var selectedItem = SettingsNavView.SelectedItem as FluentAvalonia.UI.Controls.NavigationViewItem;
var tag = selectedItem?.Tag?.ToString();
if (selectedIndex == 8)
WallpaperSettingsPanel.IsVisible = tag == "Wallpaper";
GridSettingsPanel.IsVisible = tag == "Grid";
ColorSettingsPanel.IsVisible = tag == "Color";
StatusBarSettingsPanel.IsVisible = tag == "StatusBar";
WeatherSettingsPanel.IsVisible = tag == "Weather";
RegionSettingsPanel.IsVisible = tag == "Region";
UpdateSettingsPanel.IsVisible = tag == "Update";
AboutSettingsPanel.IsVisible = tag == "About";
LauncherSettingsPanel.IsVisible = tag == "Launcher";
PluginSettingsPanel.IsVisible = tag == "Plugins";
UpdatePluginSettingsPageVisibility(tag);
if (tag == "Launcher")
{
RenderLauncherHiddenItemsList();
}
if (selectedIndex == 1)
if (tag == "Grid")
{
UpdateGridPreviewLayout();
}
@@ -864,47 +914,160 @@ public partial class MainWindow
return;
}
var snapshot = new AppSettingsSnapshot
_appSettingsService.Save(BuildAppSettingsSnapshot());
_desktopLayoutSettingsService.Save(BuildDesktopLayoutSettingsSnapshot());
_launcherSettingsService.Save(BuildLauncherSettingsSnapshot());
}
internal void ReloadFromPersistedSettings()
{
_suppressSettingsPersistence = true;
try
{
var snapshot = _appSettingsService.Load();
var desktopLayoutSnapshot = _desktopLayoutSettingsService.Load();
var launcherSnapshot = _launcherSettingsService.Load();
if (!string.IsNullOrWhiteSpace(snapshot.TimeZoneId))
{
_timeZoneService.SetTimeZoneById(snapshot.TimeZoneId);
}
_targetShortSideCells = Math.Clamp(
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
MinShortSideCells,
MaxShortSideCells);
_gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
_suppressGridSpacingEvents = true;
GridSpacingPresetComboBox.SelectedIndex =
string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
_suppressGridSpacingEvents = false;
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_suppressGridInsetEvents = true;
GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
_suppressGridInsetEvents = false;
_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;
GridSizeNumberBox.Value = _targetShortSideCells;
GridSizeSlider.Value = _targetShortSideCells;
RestoreSettingsTabSelection(snapshot);
UpdateSettingsTabContent();
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
_defaultDesktopBackground = DesktopWallpaperLayer.Background;
ApplyTaskbarSettings(snapshot);
InitializeLocalization(snapshot.LanguageCode);
InitializeWeatherSettings(snapshot);
_ = _componentSettingsService.Load();
InitializeAutoStartWithWindowsSetting(snapshot);
InitializeUpdateSettings(snapshot);
InitializeDesktopSurfaceState(desktopLayoutSnapshot);
InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeDesktopComponentPlacements(desktopLayoutSnapshot);
InitializeSettingsIcons();
TryRestoreWallpaper(snapshot.WallpaperPath);
ApplyWallpaperBrush();
UpdateWallpaperDisplay();
if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
{
_selectedThemeColor = savedThemeColor;
}
_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);
RebuildDesktopGrid();
LoadLauncherEntriesAsync();
InitializeTimeZoneSettings();
ClockWidget.SetTimeZoneService(_timeZoneService);
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
UpdateAdaptiveTextSystem();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
finally
{
_suppressSettingsPersistence = false;
}
}
private AppSettingsSnapshot BuildAppSettingsSnapshot()
{
var snapshot = _appSettingsService.Load();
snapshot.GridShortSideCells = _targetShortSideCells;
snapshot.GridSpacingPreset = _gridSpacingPreset;
snapshot.DesktopEdgeInsetPercent = _desktopEdgeInsetPercent;
snapshot.IsNightMode = _isNightMode;
snapshot.ThemeColor = _selectedThemeColor.ToString();
snapshot.WallpaperPath = _wallpaperPath;
snapshot.WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement());
snapshot.SettingsTabIndex = Math.Max(0, GetSettingsTabIndex());
snapshot.SettingsTabTag = GetSelectedSettingsTabTag();
snapshot.LanguageCode = _languageCode;
snapshot.TimeZoneId = _timeZoneService.CurrentTimeZone.Id;
snapshot.WeatherLocationMode = ToWeatherLocationModeTag(_weatherLocationMode);
snapshot.WeatherLocationKey = _weatherLocationKey;
snapshot.WeatherLocationName = _weatherLocationName;
snapshot.WeatherLatitude = _weatherLatitude;
snapshot.WeatherLongitude = _weatherLongitude;
snapshot.WeatherAutoRefreshLocation = _weatherAutoRefreshLocation;
snapshot.WeatherLocationQuery = BuildLegacyWeatherLocationQuery();
snapshot.WeatherExcludedAlerts = _weatherExcludedAlertsRaw;
snapshot.WeatherIconPackId = _weatherIconPackId;
snapshot.WeatherNoTlsRequests = _weatherNoTlsRequests;
snapshot.AutoStartWithWindows = _autoStartWithWindows;
snapshot.AutoCheckUpdates = _autoCheckUpdates;
snapshot.IncludePrereleaseUpdates = IncludePrereleaseUpdates;
snapshot.UpdateChannel = IncludePrereleaseUpdates ? UpdateChannelPreview : UpdateChannelStable;
snapshot.TopStatusComponentIds = _topStatusComponentIds.ToList();
snapshot.PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList();
snapshot.EnableDynamicTaskbarActions = _enableDynamicTaskbarActions;
snapshot.TaskbarLayoutMode = _taskbarLayoutMode;
snapshot.ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond";
snapshot.StatusBarSpacingMode = _statusBarSpacingMode;
snapshot.StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent;
return snapshot;
}
private DesktopLayoutSettingsSnapshot BuildDesktopLayoutSettingsSnapshot()
{
return new DesktopLayoutSettingsSnapshot
{
GridShortSideCells = _targetShortSideCells,
GridSpacingPreset = _gridSpacingPreset,
DesktopEdgeInsetPercent = _desktopEdgeInsetPercent,
IsNightMode = _isNightMode,
ThemeColor = _selectedThemeColor.ToString(),
WallpaperPath = _wallpaperPath,
WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement()),
SettingsTabIndex = Math.Max(0, SettingsNavListBox?.SelectedIndex ?? 0),
LanguageCode = _languageCode,
TimeZoneId = _timeZoneService.CurrentTimeZone.Id,
WeatherLocationMode = ToWeatherLocationModeTag(_weatherLocationMode),
WeatherLocationKey = _weatherLocationKey,
WeatherLocationName = _weatherLocationName,
WeatherLatitude = _weatherLatitude,
WeatherLongitude = _weatherLongitude,
WeatherAutoRefreshLocation = _weatherAutoRefreshLocation,
WeatherLocationQuery = BuildLegacyWeatherLocationQuery(),
WeatherExcludedAlerts = _weatherExcludedAlertsRaw,
WeatherIconPackId = _weatherIconPackId,
WeatherNoTlsRequests = _weatherNoTlsRequests,
AutoStartWithWindows = _autoStartWithWindows,
AutoCheckUpdates = _autoCheckUpdates,
IncludePrereleaseUpdates = IncludePrereleaseUpdates,
UpdateChannel = IncludePrereleaseUpdates ? "Preview" : "Stable",
TopStatusComponentIds = _topStatusComponentIds.ToList(),
PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(),
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
TaskbarLayoutMode = _taskbarLayoutMode,
ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond",
StatusBarSpacingMode = _statusBarSpacingMode,
StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent,
DesktopPageCount = _desktopPageCount,
CurrentDesktopSurfaceIndex = _currentDesktopSurfaceIndex,
DesktopComponentPlacements = _desktopComponentPlacements.ToList(),
DesktopComponentPlacements = _desktopComponentPlacements.ToList()
};
}
private LauncherSettingsSnapshot BuildLauncherSettingsSnapshot()
{
return new LauncherSettingsSnapshot
{
HiddenLauncherFolderPaths = _hiddenLauncherFolderPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToList(),
HiddenLauncherAppPaths = _hiddenLauncherAppPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToList()
};
_appSettingsService.Save(snapshot);
}
private IDisposable? _persistSettingsDebounceTimer;
@@ -1753,7 +1916,7 @@ public partial class MainWindow
: CalculateCurrentBackgroundLuminance() >= LightBackgroundLuminanceThreshold;
var isLightNavBackground = _isSettingsOpen
? !_isNightMode
: CalculateBrushLuminance(SettingsNavPanelBorder?.Background) >= LightBackgroundLuminanceThreshold;
: CalculateBrushLuminance(SettingsNavView?.Background) >= LightBackgroundLuminanceThreshold;
var context = new ThemeColorContext(
_selectedThemeColor,
isLightBackground,
@@ -2287,5 +2450,217 @@ public partial class MainWindow
IconVariant = IconVariant.Regular
};
}
// ========================================================================
// --- UserControl Forwarding Properties (Definitive Catch-All List) ---
// These properties allow legacy code in MainWindow.axaml.cs and other partials
// to access controls that have been moved into independent UserControls.
// ========================================================================
// --- WallpaperSettingsPage ---
internal TextBlock WallpaperPanelTitleTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPanelTitleTextBlock")!;
internal TextBlock WallpaperPathTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPathTextBlock")!;
internal TextBlock WallpaperStatusTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperStatusTextBlock")!;
internal ComboBox WallpaperPlacementComboBox => WallpaperSettingsPanel.FindControl<ComboBox>("WallpaperPlacementComboBox")!;
internal Border WallpaperPreviewHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewHost")!;
internal Border WallpaperPreviewFrame => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewFrame")!;
internal Border WallpaperPreviewViewport => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewViewport")!;
internal LibVLCSharp.Avalonia.VideoView? WallpaperPreviewVideoView => WallpaperSettingsPanel.FindControl<LibVLCSharp.Avalonia.VideoView>("WallpaperPreviewVideoView");
internal Grid WallpaperPreviewGrid => WallpaperSettingsPanel.FindControl<Grid>("WallpaperPreviewGrid")!;
internal Border WallpaperPreviewTopStatusBarHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTopStatusBarHost")!;
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel")!;
internal LanMountainDesktop.Views.Components.ClockWidget WallpaperPreviewClockWidget => WallpaperSettingsPanel.FindControl<LanMountainDesktop.Views.Components.ClockWidget>("WallpaperPreviewClockWidget")!;
internal Border WallpaperPreviewBottomTaskbarContainer => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewBottomTaskbarContainer")!;
internal Border WallpaperPreviewTaskbarFixedActionsHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTaskbarFixedActionsHost")!;
internal StackPanel WallpaperPreviewBackButtonVisual => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewBackButtonVisual")!;
internal TextBlock WallpaperPreviewBackButtonTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPreviewBackButtonTextBlock")!;
internal StackPanel WallpaperPreviewTaskbarDynamicActionsHost => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewTaskbarDynamicActionsHost")!;
internal Border WallpaperPreviewTaskbarSettingsActionHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTaskbarSettingsActionHost")!;
internal StackPanel WallpaperPreviewComponentLibraryVisual => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewComponentLibraryVisual")!;
internal TextBlock WallpaperPreviewComponentLibraryTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPreviewComponentLibraryTextBlock")!;
internal FluentIcons.Avalonia.SymbolIcon WallpaperPreviewSettingsButtonIcon => WallpaperSettingsPanel.FindControl<FluentIcons.Avalonia.SymbolIcon>("WallpaperPreviewSettingsButtonIcon")!;
internal Button PickWallpaperButton => WallpaperSettingsPanel.FindControl<Button>("PickWallpaperButton")!;
internal Button ClearWallpaperButton => WallpaperSettingsPanel.FindControl<Button>("ClearWallpaperButton")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WallpaperPlacementSettingsExpander => WallpaperSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WallpaperPlacementSettingsExpander")!;
// --- GridSettingsPage ---
internal TextBlock GridPanelTitleTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPanelTitleTextBlock")!;
internal Border GridPreviewHost => GridSettingsPanel.FindControl<Border>("GridPreviewHost")!;
internal Border GridPreviewFrame => GridSettingsPanel.FindControl<Border>("GridPreviewFrame")!;
internal Border GridPreviewViewport => GridSettingsPanel.FindControl<Border>("GridPreviewViewport")!;
internal Canvas GridPreviewLinesCanvas => GridSettingsPanel.FindControl<Canvas>("GridPreviewLinesCanvas")!;
internal Grid GridPreviewGrid => GridSettingsPanel.FindControl<Grid>("GridPreviewGrid")!;
internal Border GridPreviewTopStatusBarHost => GridSettingsPanel.FindControl<Border>("GridPreviewTopStatusBarHost")!;
internal StackPanel GridPreviewTopStatusComponentsPanel => GridSettingsPanel.FindControl<StackPanel>("GridPreviewTopStatusComponentsPanel")!;
internal Border GridPreviewBottomTaskbarContainer => GridSettingsPanel.FindControl<Border>("GridPreviewBottomTaskbarContainer")!;
internal Border GridPreviewTaskbarFixedActionsHost => GridSettingsPanel.FindControl<Border>("GridPreviewTaskbarFixedActionsHost")!;
internal StackPanel GridPreviewBackButtonVisual => GridSettingsPanel.FindControl<StackPanel>("GridPreviewBackButtonVisual")!;
internal TextBlock GridPreviewBackButtonTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPreviewBackButtonTextBlock")!;
internal StackPanel GridPreviewTaskbarDynamicActionsHost => GridSettingsPanel.FindControl<StackPanel>("GridPreviewTaskbarDynamicActionsHost")!;
internal Border GridPreviewTaskbarSettingsActionHost => GridSettingsPanel.FindControl<Border>("GridPreviewTaskbarSettingsActionHost")!;
internal StackPanel GridPreviewComponentLibraryVisual => GridSettingsPanel.FindControl<StackPanel>("GridPreviewComponentLibraryVisual")!;
internal FluentIcons.Avalonia.FluentIcon GridPreviewComponentLibraryIcon => GridSettingsPanel.FindControl<FluentIcons.Avalonia.FluentIcon>("GridPreviewComponentLibraryIcon")!;
internal TextBlock GridPreviewComponentLibraryTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPreviewComponentLibraryTextBlock")!;
internal FluentIcons.Avalonia.SymbolIcon GridPreviewSettingsButtonIcon => GridSettingsPanel.FindControl<FluentIcons.Avalonia.SymbolIcon>("GridPreviewSettingsButtonIcon")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridRowsSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridRowsSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridSpacingSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridSpacingSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridEdgeInsetSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridEdgeInsetSettingsExpander")!;
internal Slider GridSizeSlider => GridSettingsPanel.FindControl<Slider>("GridSizeSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox GridSizeNumberBox => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("GridSizeNumberBox")!;
internal ComboBox GridSpacingPresetComboBox => GridSettingsPanel.FindControl<ComboBox>("GridSpacingPresetComboBox")!;
internal ComboBoxItem GridSpacingRelaxedComboBoxItem => GridSettingsPanel.FindControl<ComboBoxItem>("GridSpacingRelaxedComboBoxItem")!;
internal ComboBoxItem GridSpacingCompactComboBoxItem => GridSettingsPanel.FindControl<ComboBoxItem>("GridSpacingCompactComboBoxItem")!;
internal Slider GridEdgeInsetSlider => GridSettingsPanel.FindControl<Slider>("GridEdgeInsetSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox GridEdgeInsetNumberBox => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("GridEdgeInsetNumberBox")!;
internal TextBlock GridEdgeInsetComputedPxTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridEdgeInsetComputedPxTextBlock")!;
internal Button ApplyGridButton => GridSettingsPanel.FindControl<Button>("ApplyGridButton")!;
internal TextBlock GridInfoTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridInfoTextBlock")!;
// --- ColorSettingsPage ---
internal TextBlock ColorPanelTitleTextBlock => ColorSettingsPanel.FindControl<TextBlock>("ColorPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeModeSettingsExpander => ColorSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeModeSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeColorSettingsExpander => ColorSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeColorSettingsExpander")!;
internal ToggleSwitch NightModeToggleSwitch => ColorSettingsPanel.FindControl<ToggleSwitch>("NightModeToggleSwitch")!;
internal TextBlock ThemeColorStatusTextBlock => ColorSettingsPanel.FindControl<TextBlock>("ThemeColorStatusTextBlock")!;
internal TextBlock RecommendedColorsLabelTextBlock => ColorSettingsPanel.FindControl<TextBlock>("RecommendedColorsLabelTextBlock")!;
internal TextBlock SystemMonetColorsLabelTextBlock => ColorSettingsPanel.FindControl<TextBlock>("SystemMonetColorsLabelTextBlock")!;
internal Button RecommendedColorButton1 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton1")!;
internal Button RecommendedColorButton2 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton2")!;
internal Button RecommendedColorButton3 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton3")!;
internal Button RecommendedColorButton4 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton4")!;
internal Button RecommendedColorButton5 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton5")!;
internal Button RecommendedColorButton6 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton6")!;
internal Border RecommendedColorSwatch1 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch1")!;
internal Border RecommendedColorSwatch2 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch2")!;
internal Border RecommendedColorSwatch3 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch3")!;
internal Border RecommendedColorSwatch4 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch4")!;
internal Border RecommendedColorSwatch5 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch5")!;
internal Border RecommendedColorSwatch6 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch6")!;
internal Button RefreshMonetColorsButton => ColorSettingsPanel.FindControl<Button>("RefreshMonetColorsButton")!;
internal Button MonetColorButton1 => ColorSettingsPanel.FindControl<Button>("MonetColorButton1")!;
internal Button MonetColorButton2 => ColorSettingsPanel.FindControl<Button>("MonetColorButton2")!;
internal Button MonetColorButton3 => ColorSettingsPanel.FindControl<Button>("MonetColorButton3")!;
internal Button MonetColorButton4 => ColorSettingsPanel.FindControl<Button>("MonetColorButton4")!;
internal Button MonetColorButton5 => ColorSettingsPanel.FindControl<Button>("MonetColorButton5")!;
internal Button MonetColorButton6 => ColorSettingsPanel.FindControl<Button>("MonetColorButton6")!;
internal Border MonetColorSwatch1 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch1")!;
internal Border MonetColorSwatch2 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch2")!;
internal Border MonetColorSwatch3 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch3")!;
internal Border MonetColorSwatch4 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch4")!;
internal Border MonetColorSwatch5 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch5")!;
internal Border MonetColorSwatch6 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch6")!;
// --- StatusBarSettingsPage ---
internal TextBlock StatusBarPanelTitleTextBlock => StatusBarSettingsPanel.FindControl<TextBlock>("StatusBarPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarClockSettingsExpander => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarClockSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarSpacingSettingsExpander => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarSpacingSettingsExpander")!;
internal ToggleSwitch StatusBarClockToggleSwitch => StatusBarSettingsPanel.FindControl<ToggleSwitch>("StatusBarClockToggleSwitch")!;
internal RadioButton ClockFormatHMSSRadio => StatusBarSettingsPanel.FindControl<RadioButton>("ClockFormatHMSSRadio")!;
internal RadioButton ClockFormatHMRadio => StatusBarSettingsPanel.FindControl<RadioButton>("ClockFormatHMRadio")!;
internal ComboBox StatusBarSpacingModeComboBox => StatusBarSettingsPanel.FindControl<ComboBox>("StatusBarSpacingModeComboBox")!;
internal ComboBoxItem StatusBarSpacingModeCompactItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeCompactItem")!;
internal ComboBoxItem StatusBarSpacingModeRelaxedItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeRelaxedItem")!;
internal ComboBoxItem StatusBarSpacingModeCustomItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeCustomItem")!;
internal FluentAvalonia.UI.Controls.SettingsExpanderItem StatusBarSpacingCustomPanel => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpanderItem>("StatusBarSpacingCustomPanel")!;
internal Slider StatusBarSpacingSlider => StatusBarSettingsPanel.FindControl<Slider>("StatusBarSpacingSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox StatusBarSpacingNumberBox => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("StatusBarSpacingNumberBox")!;
internal TextBlock StatusBarSpacingComputedPxTextBlock => StatusBarSettingsPanel.FindControl<TextBlock>("StatusBarSpacingComputedPxTextBlock")!;
// --- RegionSettingsPage ---
internal TextBlock RegionPanelTitleTextBlock => RegionSettingsPanel.FindControl<TextBlock>("RegionPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander LanguageSettingsExpander => RegionSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("LanguageSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander TimeZoneSettingsExpander => RegionSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("TimeZoneSettingsExpander")!;
internal ComboBox LanguageComboBox => RegionSettingsPanel.FindControl<ComboBox>("LanguageComboBox")!;
internal ComboBoxItem LanguageChineseItem => RegionSettingsPanel.FindControl<ComboBoxItem>("LanguageChineseItem")!;
internal ComboBoxItem LanguageEnglishItem => RegionSettingsPanel.FindControl<ComboBoxItem>("LanguageEnglishItem")!;
internal ComboBox TimeZoneComboBox => RegionSettingsPanel.FindControl<ComboBox>("TimeZoneComboBox")!;
// --- WeatherSettingsPage ---
internal TextBlock WeatherPanelTitleTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherPreviewSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherPreviewSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherLocationSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherLocationSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCitySearchSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCitySearchSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCoordinateSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCoordinateSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherAlertFilterSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherAlertFilterSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherIconPackSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherIconPackSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherNoTlsSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherNoTlsSettingsExpander")!;
internal Button WeatherPreviewButton => WeatherSettingsPanel.FindControl<Button>("WeatherPreviewButton")!;
internal ComboBox WeatherLocationModeComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherLocationModeComboBox")!;
internal ComboBoxItem WeatherLocationModeCityItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherLocationModeCityItem")!;
internal ComboBoxItem WeatherLocationModeCoordinatesItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherLocationModeCoordinatesItem")!;
internal ListBoxItem WeatherLocationModeCityChipItem => WeatherSettingsPanel.FindControl<ListBoxItem>("WeatherLocationModeCityChipItem")!;
internal ListBoxItem WeatherLocationModeCoordinatesChipItem => WeatherSettingsPanel.FindControl<ListBoxItem>("WeatherLocationModeCoordinatesChipItem")!;
internal ListBox WeatherLocationModeChipListBox => WeatherSettingsPanel.FindControl<ListBox>("WeatherLocationModeChipListBox")!;
internal ToggleSwitch WeatherAutoRefreshToggleSwitch => WeatherSettingsPanel.FindControl<ToggleSwitch>("WeatherAutoRefreshToggleSwitch")!;
internal Button WeatherSearchButton => WeatherSettingsPanel.FindControl<Button>("WeatherSearchButton")!;
internal Button WeatherApplyCityButton => WeatherSettingsPanel.FindControl<Button>("WeatherApplyCityButton")!;
internal Button WeatherApplyCoordinatesButton => WeatherSettingsPanel.FindControl<Button>("WeatherApplyCoordinatesButton")!;
internal TextBox WeatherExcludedAlertsTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherExcludedAlertsTextBox")!;
internal ComboBox WeatherIconPackComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherIconPackComboBox")!;
internal ToggleSwitch WeatherNoTlsToggleSwitch => WeatherSettingsPanel.FindControl<ToggleSwitch>("WeatherNoTlsToggleSwitch")!;
internal TextBox WeatherCitySearchTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherCitySearchTextBox")!;
internal ComboBox WeatherCityResultsComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherCityResultsComboBox")!;
internal TextBlock WeatherSearchStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherSearchStatusTextBlock")!;
internal TextBox WeatherLocationKeyTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherLocationKeyTextBox")!;
internal TextBox WeatherLocationNameTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherLocationNameTextBox")!;
internal FluentAvalonia.UI.Controls.NumberBox WeatherLatitudeNumberBox => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLatitudeNumberBox")!;
internal FluentAvalonia.UI.Controls.NumberBox WeatherLongitudeNumberBox => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLongitudeNumberBox")!;
internal TextBlock WeatherCoordinateStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherCoordinateStatusTextBlock")!;
internal TextBlock WeatherPreviewResultTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewResultTextBlock")!;
internal FluentIcons.Avalonia.Fluent.SymbolIcon WeatherPreviewIconSymbol => WeatherSettingsPanel.FindControl<FluentIcons.Avalonia.Fluent.SymbolIcon>("WeatherPreviewIconSymbol")!;
internal TextBlock WeatherPreviewTemperatureTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewTemperatureTextBlock")!;
internal TextBlock WeatherPreviewUpdatedTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewUpdatedTextBlock")!;
internal FluentAvalonia.UI.Controls.ProgressRing WeatherSearchProgressRing => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherSearchProgressRing")!;
internal FluentAvalonia.UI.Controls.ProgressRing WeatherPreviewProgressRing => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherPreviewProgressRing")!;
internal ComboBoxItem WeatherIconPackFluentRegularItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherIconPackFluentRegularItem")!;
internal ComboBoxItem WeatherIconPackFluentFilledItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherIconPackFluentFilledItem")!;
internal TextBlock WeatherLocationStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherLocationStatusTextBlock")!;
// --- UpdateSettingsPage ---
internal TextBlock UpdatePanelTitleTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateOptionsSettingsExpander => UpdateSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateOptionsSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateActionsSettingsExpander => UpdateSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateActionsSettingsExpander")!;
internal TextBlock UpdateCurrentVersionLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateCurrentVersionLabelTextBlock")!;
internal TextBlock UpdateCurrentVersionValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateCurrentVersionValueTextBlock")!;
internal TextBlock UpdateLatestVersionLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateLatestVersionLabelTextBlock")!;
internal TextBlock UpdateLatestVersionValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateLatestVersionValueTextBlock")!;
internal TextBlock UpdatePublishedAtLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePublishedAtLabelTextBlock")!;
internal TextBlock UpdatePublishedAtValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePublishedAtValueTextBlock")!;
internal TextBlock UpdateChannelLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateChannelLabelTextBlock")!;
internal ListBoxItem UpdateChannelStableChipItem => UpdateSettingsPanel.FindControl<ListBoxItem>("UpdateChannelStableChipItem")!;
internal ListBoxItem UpdateChannelPreviewChipItem => UpdateSettingsPanel.FindControl<ListBoxItem>("UpdateChannelPreviewChipItem")!;
internal ToggleSwitch AutoCheckUpdatesToggleSwitch => UpdateSettingsPanel.FindControl<ToggleSwitch>("AutoCheckUpdatesToggleSwitch")! ;
internal ListBox UpdateChannelChipListBox => UpdateSettingsPanel.FindControl<ListBox>("UpdateChannelChipListBox")!;
internal Button CheckForUpdatesButton => UpdateSettingsPanel.FindControl<Button>("CheckForUpdatesButton")!;
internal Button DownloadAndInstallUpdateButton => UpdateSettingsPanel.FindControl<Button>("DownloadAndInstallUpdateButton")!;
internal ProgressBar UpdateDownloadProgressBar => UpdateSettingsPanel.FindControl<ProgressBar>("UpdateDownloadProgressBar")!;
internal TextBlock UpdateDownloadProgressTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateDownloadProgressTextBlock")!;
internal TextBlock UpdateStatusTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateStatusTextBlock")!;
// --- AboutSettingsPage ---
internal TextBlock AboutPanelTitleTextBlock => AboutSettingsPanel.FindControl<TextBlock>("AboutPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander AboutStartupSettingsExpander => AboutSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutStartupSettingsExpander")!;
internal ToggleSwitch AutoStartWithWindowsToggleSwitch => AboutSettingsPanel.FindControl<ToggleSwitch>("AutoStartWithWindowsToggleSwitch")!;
internal TextBlock VersionTextBlock => AboutSettingsPanel.FindControl<TextBlock>("VersionTextBlock")!;
internal TextBlock CodeNameTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CodeNameTextBlock")!;
internal TextBlock FontInfoTextBlock => AboutSettingsPanel.FindControl<TextBlock>("FontInfoTextBlock")!;
// --- LauncherSettingsPage ---
internal TextBlock LauncherSettingsPanelTitleTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherSettingsPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander LauncherHiddenItemsSettingsExpander => LauncherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("LauncherHiddenItemsSettingsExpander")!;
internal StackPanel LauncherHiddenItemsListPanel => LauncherSettingsPanel.FindControl<StackPanel>("LauncherHiddenItemsListPanel")!;
internal TextBlock LauncherHiddenItemsEmptyTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherHiddenItemsEmptyTextBlock")!;
internal TextBlock LauncherHiddenItemsDescriptionTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherHiddenItemsDescriptionTextBlock")!;
// --- PluginSettingsPage (Added for completeness) ---
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 TextBlock PluginRestartHintTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginRestartHintTextBlock")!;
internal TextBlock PluginCatalogEmptyTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginCatalogEmptyTextBlock")!;
}

View File

@@ -78,7 +78,7 @@ public partial class MainWindow
private void ApplyUpdateLocalization()
{
SettingsNavUpdateTextBlock.Text = L("settings.nav.update", "Update");
SettingsNavUpdateItem.Content = L("settings.nav.update", "Update");
UpdatePanelTitleTextBlock.Text = L("settings.update.title", "Update");
UpdateCurrentVersionLabelTextBlock.Text = L("settings.update.current_version_label", "Current Version");

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,6 @@ using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Styling;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.ComponentSystem.Extensions;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
@@ -88,6 +87,8 @@ public partial class MainWindow : Window
}
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 LocalizationService _localizationService = new();
private readonly TimeZoneService _timeZoneService = new();
@@ -96,11 +97,7 @@ public partial class MainWindow : Window
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
private readonly ComponentRegistry _componentRegistry = ComponentRegistry
.CreateDefault()
.RegisterExtensions(
JsonComponentExtensionProvider.LoadProvidersFromDirectory(
Path.Combine(AppContext.BaseDirectory, "Extensions", "Components")));
private readonly ComponentRegistry _componentRegistry;
private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry;
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
@@ -174,17 +171,83 @@ public partial class MainWindow : Window
private bool _isWeatherSearchInProgress;
private bool _isWeatherPreviewInProgress;
private ClockDisplayFormat _clockDisplayFormat = ClockDisplayFormat.HourMinuteSecond;
private bool _externalSettingsReloadPending;
private double CurrentDesktopPitch => _currentDesktopCellSize + _currentDesktopCellGap;
public MainWindow()
{
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
InitializeComponent();
_componentRuntimeRegistry = DesktopComponentRuntimeRegistry.CreateDefault(_componentRegistry);
InitializePluginSettingsNavigation();
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService);
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
AppSettingsService.SettingsSaved += OnExternalAppSettingsSaved;
LauncherSettingsService.SettingsSaved += OnExternalLauncherSettingsSaved;
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.Checked += OnNightModeChecked;
NightModeToggleSwitch.Unchecked += OnNightModeUnchecked;
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.Checked += OnStatusBarClockChecked;
StatusBarClockToggleSwitch.Unchecked += OnStatusBarClockUnchecked;
ClockFormatHMSSRadio.Checked += OnClockFormatChanged;
ClockFormatHMRadio.Checked += OnClockFormatChanged;
StatusBarSpacingModeComboBox.SelectionChanged += OnStatusBarSpacingModeChanged;
StatusBarSpacingSlider.ValueChanged += OnStatusBarSpacingSliderChanged;
WeatherPreviewButton.Click += OnTestWeatherRequestClick;
WeatherLocationModeComboBox.SelectionChanged += OnWeatherLocationModeSelectionChanged;
WeatherLocationModeChipListBox.SelectionChanged += OnWeatherLocationModeChipSelectionChanged;
WeatherAutoRefreshToggleSwitch.Checked += OnWeatherAutoRefreshToggled;
WeatherAutoRefreshToggleSwitch.Unchecked += OnWeatherAutoRefreshToggled;
WeatherSearchButton.Click += OnSearchWeatherCityClick;
WeatherApplyCityButton.Click += OnApplyWeatherCitySelectionClick;
WeatherApplyCoordinatesButton.Click += OnApplyWeatherCoordinatesClick;
WeatherExcludedAlertsTextBox.LostFocus += OnWeatherExcludedAlertsLostFocus;
WeatherIconPackComboBox.SelectionChanged += OnWeatherIconPackSelectionChanged;
WeatherNoTlsToggleSwitch.Checked += OnWeatherNoTlsToggled;
WeatherNoTlsToggleSwitch.Unchecked += OnWeatherNoTlsToggled;
LanguageComboBox.SelectionChanged += OnLanguageSelectionChanged;
TimeZoneComboBox.SelectionChanged += OnTimeZoneSelectionChanged;
AutoCheckUpdatesToggleSwitch.Checked += OnAutoCheckUpdatesToggled;
AutoCheckUpdatesToggleSwitch.Unchecked += OnAutoCheckUpdatesToggled;
UpdateChannelChipListBox.SelectionChanged += OnUpdateChannelSelectionChanged;
CheckForUpdatesButton.Click += OnCheckForUpdatesClick;
DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick;
AutoStartWithWindowsToggleSwitch.Checked += OnAutoStartWithWindowsToggled;
AutoStartWithWindowsToggleSwitch.Unchecked += OnAutoStartWithWindowsToggled;
}
protected override void OnOpened(EventArgs e)
@@ -193,6 +256,8 @@ public partial class MainWindow : Window
_suppressSettingsPersistence = true;
var snapshot = _appSettingsService.Load();
var desktopLayoutSnapshot = _desktopLayoutSettingsService.Load();
var launcherSnapshot = _launcherSettingsService.Load();
if (!string.IsNullOrWhiteSpace(snapshot.TimeZoneId))
{
@@ -236,7 +301,7 @@ public partial class MainWindow : Window
GridSizeSlider.ValueChanged += OnGridSizeSliderChanged;
GridSizeNumberBox.ValueChanged += OnGridSizeNumberBoxChanged;
SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 9);
RestoreSettingsTabSelection(snapshot);
UpdateSettingsTabContent();
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
@@ -247,9 +312,9 @@ public partial class MainWindow : Window
_ = _componentSettingsService.Load();
InitializeAutoStartWithWindowsSetting(snapshot);
InitializeUpdateSettings(snapshot);
InitializeDesktopSurfaceState(snapshot);
InitializeLauncherVisibilitySettings(snapshot);
InitializeDesktopComponentPlacements(snapshot);
InitializeDesktopSurfaceState(desktopLayoutSnapshot);
InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeDesktopComponentPlacements(desktopLayoutSnapshot);
InitializeSettingsIcons();
TryRestoreWallpaper(snapshot.WallpaperPath);
@@ -309,6 +374,8 @@ public partial class MainWindow : Window
_releaseUpdateService.Dispose();
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
AppSettingsService.SettingsSaved -= OnExternalAppSettingsSaved;
LauncherSettingsService.SettingsSaved -= OnExternalLauncherSettingsSaved;
PropertyChanged -= OnWindowPropertyChanged;
DesktopHost.SizeChanged -= OnDesktopHostSizeChanged;
WallpaperPreviewHost.SizeChanged -= OnWallpaperPreviewHostSizeChanged;
@@ -1324,6 +1391,7 @@ public partial class MainWindow : Window
_suppressTimeZoneSelectionEvents = false;
}
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressTimeZoneSelectionEvents || TimeZoneComboBox.SelectedItem is not ComboBoxItem item)
@@ -1341,4 +1409,3 @@ public partial class MainWindow : Window
PersistSettings();
}
}

View File

@@ -0,0 +1,38 @@
<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 AdaptiveTextPrimaryBrush}" Text="About" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="LanMountainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock Text="Modern desktop shell experience." FontSize="13" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Separator Background="{DynamicResource AdaptiveButtonBorderBrush}" Margin="0,8" />
<TextBlock x:Name="VersionTextBlock" Text="Version: 1.0.0" FontSize="13" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<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 AdaptiveTextSecondaryBrush}" />
</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>
</StackPanel>
</UserControl>

View File

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

View File

@@ -0,0 +1,204 @@
<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"
Spacing="16">
<TextBlock x:Name="ColorPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="Color" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="ThemeModeSettingsExpander"
Header="&#26085;&#22812;&#27169;&#24335;"
Description="&#20999;&#25442;&#24212;&#29992;&#30340;&#27973;&#33394;&#25110;&#28145;&#33394;&#20027;&#39064;&#12290;">
<ui:SettingsExpander.IconSource>
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="NightModeToggleSwitch"
OffContent="Day"
OnContent="Night" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="ThemeColorSettingsExpander"
Header="&#20027;&#39064;&#33394;"
Description="&#36873;&#25321;&#24212;&#29992;&#30340;&#20027;&#39064;&#28857;&#32512;&#33394;&#12290;">
<ui:SettingsExpander.IconSource>
</ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<StackPanel Spacing="12">
<TextBlock x:Name="RecommendedColorsLabelTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
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 AdaptiveTextSecondaryBrush}"
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>
<TextBlock x:Name="ThemeColorStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
Text="Theme color is ready." />
</StackPanel>
</UserControl>

View File

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

View File

@@ -0,0 +1,172 @@
<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="*, *"
RowDefinitions="Auto, *">
<TextBlock x:Name="GridPanelTitleTextBlock"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="28"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Margin="0,0,0,24"
Text="调整网格布局" />
<!-- Left Column: Grid Preview -->
<Border x:Name="GridPreviewHost"
Grid.Row="1" Grid.Column="0"
Margin="0,0,16,0"
VerticalAlignment="Top"
HorizontalAlignment="Stretch">
<Border x:Name="GridPreviewFrame"
HorizontalAlignment="Stretch"
CornerRadius="28"
Background="#FF1A1A1A"
Padding="12">
<Border x:Name="GridPreviewViewport"
ClipToBounds="True"
CornerRadius="16"
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="16,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 AdaptiveTextPrimaryBrush}"
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 AdaptiveTextPrimaryBrush}"
Value="18" />
</Grid>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<TextBlock x:Name="GridEdgeInsetComputedPxTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text=">= 0 px" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<Button x:Name="ApplyGridButton"
HorizontalAlignment="Stretch"
Padding="0,10"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Content="应用" />
<TextBlock x:Name="GridInfoTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Grid: - cols x - rows (1:1)" />
</StackPanel>
</Grid>
</UserControl>

View File

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

View File

@@ -0,0 +1,45 @@
<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 AdaptiveTextPrimaryBrush}"
Text="App Launcher" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="LauncherHiddenItemsSettingsExpander"
Header="Hidden Items"
Description="Review hidden launcher entries and show them again."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe71e;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="LauncherHiddenItemsDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Right-click an icon in launcher to hide it. Hidden entries appear here." />
<TextBlock x:Name="LauncherHiddenItemsEmptyTextBlock"
IsVisible="False"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="No hidden items." />
<ScrollViewer MaxHeight="420"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel x:Name="LauncherHiddenItemsListPanel"
Spacing="8" />
</ScrollViewer>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

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

View File

@@ -0,0 +1,68 @@
<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" Spacing="16">
<TextBlock x:Name="PluginSettingsPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="Plugins" />
<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>
<ui:FontIconSource Glyph="&#xe734;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginSystemDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="This page will host installed plugin management, permission review, and sandboxed backend runtime controls." />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="14">
<TextBlock x:Name="PluginSystemStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
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="Enable plugins here. Detailed settings appear as separate settings pages."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe8fd;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginRestartHintTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
TextWrapping="Wrap"
Text="Plugin enable state changes take effect after restarting the app." />
<TextBlock x:Name="PluginCatalogEmptyTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="No plugins found."
IsVisible="False" />
<StackPanel x:Name="PluginCatalogItemsHost" Spacing="12" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,222 @@
using System;
using System.Globalization;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginSettingsPage : UserControl
{
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
public PluginSettingsPage()
{
InitializeComponent();
AttachedToVisualTree += (_, _) => RefreshFromRuntime();
}
public void RefreshFromRuntime()
{
var runtime = (Application.Current as App)?.PluginRuntimeService;
if (runtime is null)
{
PluginSystemStatusTextBlock.Text = L("settings.plugins.runtime_unavailable", "Plugin runtime is not available.");
PluginRuntimeSummaryPanel.Children.Clear();
PluginCatalogItemsHost.Children.Clear();
PluginRestartHintTextBlock.IsVisible = false;
return;
}
BuildRuntimeSummary(runtime);
BuildPluginCatalog(runtime);
}
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 pages {3}; widgets {4}; failures {5}.",
runtime.Catalog.Count,
enabledCount,
runtime.LoadedPlugins.Count,
runtime.SettingsPages.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)
{
PluginCatalogItemsHost.Children.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)
{
PluginCatalogItemsHost.Children.Add(CreatePluginCatalogItem(runtime, plugin));
}
}
private Control CreatePluginCatalogItem(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
var title = new TextBlock
{
Text = entry.Manifest.Name,
FontSize = 16,
FontWeight = FontWeight.SemiBold,
TextWrapping = TextWrapping.Wrap
};
var subtitle = new TextBlock
{
Text = BuildPluginSubtitle(entry),
Foreground = PluginSystemDescriptionTextBlock.Foreground,
TextWrapping = TextWrapping.Wrap
};
var enabledToggle = new ToggleSwitch
{
IsChecked = entry.IsEnabled,
OnContent = L("settings.plugins.toggle_on", "Enabled"),
OffContent = L("settings.plugins.toggle_off", "Disabled"),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
enabledToggle.Checked += (_, _) => OnPluginEnableChanged(runtime, entry, true);
enabledToggle.Unchecked += (_, _) => OnPluginEnableChanged(runtime, entry, false);
var header = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,Auto"),
ColumnSpacing = 12,
Children =
{
new StackPanel
{
Spacing = 4,
Children = { title, subtitle }
},
enabledToggle
}
};
Grid.SetColumn(enabledToggle, 1);
var details = new TextBlock
{
Text = BuildPluginDetails(entry),
Foreground = PluginSystemDescriptionTextBlock.Foreground,
TextWrapping = TextWrapping.Wrap
};
return new Border
{
Background = new SolidColorBrush(Color.Parse("#14000000")),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(14),
Child = new StackPanel
{
Spacing = 10,
Children = { header, details }
}
};
}
private void OnPluginEnableChanged(PluginRuntimeService runtime, PluginCatalogEntry entry, bool isEnabled)
{
runtime.SetPluginEnabled(entry.Manifest.Id, isEnabled);
BuildRuntimeSummary(runtime);
BuildPluginCatalog(runtime);
PluginSystemStatusTextBlock.Text = 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,
isEnabled
? L("settings.plugins.toggle_state_enabled", "enabled")
: L("settings.plugins.toggle_state_disabled", "disabled"));
}
private string BuildPluginSubtitle(PluginCatalogEntry entry)
{
var source = entry.IsPackage
? L("settings.plugins.source_package", ".laapp package")
: L("settings.plugins.source_manifest", "Loose manifest");
var state = entry.IsEnabled
? entry.IsLoaded
? L("settings.plugins.state.loaded", "Loaded")
: L("settings.plugins.state.load_failed", "Load failed")
: L("settings.plugins.state.disabled", "Disabled");
return F(
"settings.plugins.subtitle_format",
"{0} | {1} | {2}",
state,
source,
entry.Manifest.Id);
}
private string BuildPluginDetails(PluginCatalogEntry entry)
{
var detail = F(
"settings.plugins.detail_format",
"Settings pages: {0} | Widgets: {1}",
entry.SettingsPageCount,
entry.WidgetCount);
return string.IsNullOrWhiteSpace(entry.ErrorMessage)
? detail
: detail + Environment.NewLine + entry.ErrorMessage;
}
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);
}
}

View File

@@ -0,0 +1,49 @@
<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 AdaptiveTextPrimaryBrush}"
Text="Region" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="LanguageSettingsExpander"
Header="Language"
Description="Select application language. Changes apply immediately.">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Translate" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox x:Name="LanguageComboBox"
Width="220">
<ComboBoxItem x:Name="LanguageChineseItem" Tag="zh-CN" Content="中文" />
<ComboBoxItem x:Name="LanguageEnglishItem" Tag="en-US" Content="English" />
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="TimeZoneSettingsExpander"
Header="Time Zone"
Description="Select a time zone. Clock and calendar widgets will follow this zone.">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Clock" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox x:Name="TimeZoneComboBox"
Width="280" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

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

View File

@@ -0,0 +1,90 @@
<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 AdaptiveTextPrimaryBrush}"
Text="Status Bar" />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="StatusBarClockSettingsExpander"
Header="&#31995;&#32479;&#26102;&#38047;"
Description="&#22312;&#29366;&#24577;&#26639;&#19978;&#26174;&#31034;&#26102;&#38388;&#12290;"
IsExpanded="False">
<ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="StatusBarClockToggleSwitch"
OnContent="On"
OffContent="Off" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem Content="Display Format">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="16">
<RadioButton x:Name="ClockFormatHMSSRadio"
Content="HH:mm:ss"
GroupName="ClockFormat"
Tag="Hms" />
<RadioButton x:Name="ClockFormatHMRadio"
Content="HH:mm"
GroupName="ClockFormat"
Tag="Hm" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="StatusBarSpacingSettingsExpander"
Header="Component spacing"
Description="Adjust spacing between status bar components."
IsExpanded="False">
<ui:SettingsExpander.Footer>
<ComboBox x:Name="StatusBarSpacingModeComboBox"
Width="150">
<ComboBoxItem x:Name="StatusBarSpacingModeCompactItem" Tag="Compact" Content="Compact" />
<ComboBoxItem x:Name="StatusBarSpacingModeRelaxedItem" Tag="Relaxed" Content="Relaxed" />
<ComboBoxItem x:Name="StatusBarSpacingModeCustomItem" Tag="Custom" Content="Custom" />
</ComboBox>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem x:Name="StatusBarSpacingCustomPanel"
Content="Custom spacing"
IsVisible="False">
<ui:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal" Spacing="12">
<Slider x:Name="StatusBarSpacingSlider"
Width="150"
Minimum="0"
Maximum="30"
TickFrequency="1"
Value="12" />
<ui:NumberBox x:Name="StatusBarSpacingNumberBox"
Width="80"
Minimum="0"
Maximum="30"
Value="12" />
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<TextBlock x:Name="StatusBarSpacingComputedPxTextBlock"
HorizontalAlignment="Right"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text=">= 0 px" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

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

View File

@@ -0,0 +1,122 @@
<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 AdaptiveTextPrimaryBrush}"
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 AdaptiveTextSecondaryBrush}" />
<TextBlock x:Name="UpdateCurrentVersionValueTextBlock"
Grid.Column="1"
Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="UpdateLatestVersionLabelTextBlock"
Grid.Row="1"
Text="Latest Release"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock x:Name="UpdateLatestVersionValueTextBlock"
Grid.Row="1" Grid.Column="1"
Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="UpdatePublishedAtLabelTextBlock"
Grid.Row="2"
Text="Published At"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock x:Name="UpdatePublishedAtValueTextBlock"
Grid.Row="2" Grid.Column="1"
Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</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 AdaptiveTextSecondaryBrush}" />
<ListBox x:Name="UpdateChannelChipListBox"
Classes="settings-chip-list">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBoxItem x:Name="UpdateChannelStableChipItem"
Tag="Stable"
Content="Stable" />
<ListBoxItem x:Name="UpdateChannelPreviewChipItem"
Tag="Preview"
Content="Preview" />
</ListBox>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="UpdateActionsSettingsExpander"
Header="Update Actions"
Description="Check releases, download installer, and start update."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ArrowSync" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="CheckForUpdatesButton"
MinWidth="140"
Content="Check for Updates" />
<Button x:Name="DownloadAndInstallUpdateButton"
MinWidth="180"
Content="Download &amp; Install" />
</StackPanel>
<ProgressBar x:Name="UpdateDownloadProgressBar"
Minimum="0"
Maximum="100"
Height="6"
IsVisible="False" />
<TextBlock x:Name="UpdateDownloadProgressTextBlock"
Text="Download progress: -"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock x:Name="UpdateStatusTextBlock"
Text="Ready to check for updates."
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

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

View File

@@ -0,0 +1,151 @@
<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"
xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.WallpaperSettingsPage">
<Grid x:Name="WallpaperSettingsPanel"
ColumnDefinitions="*, *"
RowDefinitions="Auto, *">
<TextBlock x:Name="WallpaperPanelTitleTextBlock"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="28"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Margin="0,0,0,24"
Text="Personalize Wallpaper" />
<!-- Left Column: Monitor Preview -->
<Border x:Name="WallpaperPreviewHost"
Grid.Row="1" Grid.Column="0"
Margin="0,0,16,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<!-- Monitor Frame (Bezel) -->
<Border x:Name="WallpaperPreviewFrame"
HorizontalAlignment="Stretch"
CornerRadius="28"
Background="#FF1A1A1A"
Padding="12">
<Border x:Name="WallpaperPreviewViewport"
ClipToBounds="True"
CornerRadius="12"
Background="#30111827">
<Grid>
<vlc:VideoView x:Name="WallpaperPreviewVideoView"
IsVisible="False"
IsHitTestVisible="False"
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="16,0,0,0"
Spacing="16">
<StackPanel Spacing="8">
<TextBlock Text="Preview status" FontSize="12" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock x:Name="WallpaperPathTextBlock"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
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 AdaptiveTextPrimaryBrush}" />
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
<Button x:Name="PickWallpaperButton"
Grid.Column="0"
Classes="accent"
HorizontalAlignment="Stretch"
Padding="0,10"
Content="Browse" />
<Button x:Name="ClearWallpaperButton"
Grid.Column="1"
HorizontalAlignment="Stretch"
Padding="0,10"
Content="Reset" />
</Grid>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WallpaperPlacementSettingsExpander"
Header="Placement"
Padding="12,8">
<ui:SettingsExpander.Footer>
<ComboBox x:Name="WallpaperPlacementComboBox"
Width="120">
<ComboBoxItem Content="Fill" />
<ComboBoxItem Content="Fit" />
<ComboBoxItem Content="Stretch" />
<ComboBoxItem Content="Center" />
<ComboBoxItem Content="Tile" />
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</Grid>
</UserControl>

View File

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

View File

@@ -0,0 +1,258 @@
<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="1200"
x:Class="LanMountainDesktop.Views.SettingsPages.WeatherSettingsPage">
<StackPanel x:Name="WeatherSettingsContentPanel"
Margin="0,0,8,0"
Spacing="16">
<TextBlock x:Name="WeatherPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="Weather" />
<!-- Weather Preview Card -->
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherPreviewSettingsExpander"
Header="Weather Preview"
Description="Refresh and verify current weather service status."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="WeatherSunny" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Name="WeatherPreviewButton"
Padding="12,8"
Content="Refresh" />
<ui:ProgressRing x:Name="WeatherPreviewProgressRing"
Width="20"
Height="20"
IsActive="True"
IsVisible="False" />
</StackPanel>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
<Border Width="44"
Height="44"
CornerRadius="{DynamicResource DesignCornerRadiusXs}"
BorderThickness="1"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
Background="{DynamicResource AdaptiveButtonBackgroundBrush}">
<fi:SymbolIcon x:Name="WeatherPreviewIconSymbol"
Symbol="WeatherSunny"
IconVariant="Regular"
FontSize="22"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="WeatherPreviewTemperatureTextBlock"
FontSize="22"
FontWeight="SemiBold"
Text="--°" />
<TextBlock x:Name="WeatherPreviewUpdatedTextBlock"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="-" />
</StackPanel>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<TextBlock x:Name="WeatherPreviewResultTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
TextWrapping="Wrap"
Text="Use refresh to verify your weather configuration." />
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<!-- Location Source Card -->
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherLocationSettingsExpander"
Header="Location Source"
Description="Choose how weather widgets resolve location."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Location" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ListBox x:Name="WeatherLocationModeChipListBox"
Classes="settings-chip-list"
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>
<ui:SettingsExpanderItem.Footer>
<ToggleSwitch x:Name="WeatherAutoRefreshToggleSwitch"
Content="Auto refresh location on startup" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
<!-- ComboBox hidden as in original -->
<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>
<!-- City Search Card -->
<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="12,8"
Content="Apply City" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem Content="Advanced Filters">
<StackPanel Spacing="10">
<Grid ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
<TextBox x:Name="WeatherCitySearchTextBox"
Watermark="e.g. Beijing" />
<ui:ProgressRing x:Name="WeatherSearchProgressRing"
Grid.Column="1"
Width="24"
Height="24"
IsActive="True"
IsVisible="False" />
<Button x:Name="WeatherSearchButton"
Grid.Column="2"
Padding="12,8"
Content="Search" />
</Grid>
<ComboBox x:Name="WeatherCityResultsComboBox"
Width="320" />
<TextBlock x:Name="WeatherSearchStatusTextBlock"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Search by city name and apply one location." />
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<!-- Coordinates Card -->
<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="12,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 AdaptiveTextSecondaryBrush}" />
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<!-- Excluded Alerts Card -->
<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>
<TextBox x:Name="WeatherExcludedAlertsTextBox"
MinHeight="96"
MaxHeight="220"
Width="360"
TextWrapping="Wrap"
AcceptsReturn="True" />
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</Border>
<!-- Weather Style Card -->
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="WeatherIconPackSettingsExpander"
Header="Weather Icon Style"
Description="Choose Fluent Icon style for weather symbols."
IsExpanded="True">
<ui:SettingsExpander.Footer>
<ComboBox x:Name="WeatherIconPackComboBox"
Width="240">
<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>
<!-- No TLS Card -->
<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" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<TextBlock x:Name="WeatherLocationStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="No city location is configured." />
</StackPanel>
</UserControl>

View File

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

View File

@@ -0,0 +1,213 @@
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using LibVLCSharp.Avalonia;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
// --- WallpaperSettingsPage ---
internal TextBlock WallpaperPanelTitleTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPanelTitleTextBlock")!;
internal TextBlock WallpaperPathTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPathTextBlock")!;
internal TextBlock WallpaperStatusTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperStatusTextBlock")!;
internal ComboBox WallpaperPlacementComboBox => WallpaperSettingsPanel.FindControl<ComboBox>("WallpaperPlacementComboBox")!;
internal Border WallpaperPreviewHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewHost")!;
internal Border WallpaperPreviewFrame => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewFrame")!;
internal Border WallpaperPreviewViewport => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewViewport")!;
internal LibVLCSharp.Avalonia.VideoView? WallpaperPreviewVideoView => WallpaperSettingsPanel.FindControl<LibVLCSharp.Avalonia.VideoView>("WallpaperPreviewVideoView");
internal Grid WallpaperPreviewGrid => WallpaperSettingsPanel.FindControl<Grid>("WallpaperPreviewGrid")!;
internal Border WallpaperPreviewTopStatusBarHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTopStatusBarHost")!;
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel")!;
internal LanMountainDesktop.Views.Components.ClockWidget WallpaperPreviewClockWidget => WallpaperSettingsPanel.FindControl<LanMountainDesktop.Views.Components.ClockWidget>("WallpaperPreviewClockWidget")!;
internal Border WallpaperPreviewBottomTaskbarContainer => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewBottomTaskbarContainer")!;
internal Border WallpaperPreviewTaskbarFixedActionsHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTaskbarFixedActionsHost")!;
internal StackPanel WallpaperPreviewBackButtonVisual => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewBackButtonVisual")!;
internal TextBlock WallpaperPreviewBackButtonTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPreviewBackButtonTextBlock")!;
internal StackPanel WallpaperPreviewTaskbarDynamicActionsHost => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewTaskbarDynamicActionsHost")!;
internal Border WallpaperPreviewTaskbarSettingsActionHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTaskbarSettingsActionHost")!;
internal StackPanel WallpaperPreviewComponentLibraryVisual => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewComponentLibraryVisual")!;
internal TextBlock WallpaperPreviewComponentLibraryTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPreviewComponentLibraryTextBlock")!;
internal FluentIcons.Avalonia.SymbolIcon WallpaperPreviewSettingsButtonIcon => WallpaperSettingsPanel.FindControl<FluentIcons.Avalonia.SymbolIcon>("WallpaperPreviewSettingsButtonIcon")!;
internal Button PickWallpaperButton => WallpaperSettingsPanel.FindControl<Button>("PickWallpaperButton")!;
internal Button ClearWallpaperButton => WallpaperSettingsPanel.FindControl<Button>("ClearWallpaperButton")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WallpaperPlacementSettingsExpander => WallpaperSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WallpaperPlacementSettingsExpander")!;
// --- GridSettingsPage ---
internal TextBlock GridPanelTitleTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPanelTitleTextBlock")!;
internal Border GridPreviewHost => GridSettingsPanel.FindControl<Border>("GridPreviewHost")!;
internal Border GridPreviewFrame => GridSettingsPanel.FindControl<Border>("GridPreviewFrame")!;
internal Border GridPreviewViewport => GridSettingsPanel.FindControl<Border>("GridPreviewViewport")!;
internal Canvas GridPreviewLinesCanvas => GridSettingsPanel.FindControl<Canvas>("GridPreviewLinesCanvas")!;
internal Grid GridPreviewGrid => GridSettingsPanel.FindControl<Grid>("GridPreviewGrid")!;
internal Border GridPreviewTopStatusBarHost => GridSettingsPanel.FindControl<Border>("GridPreviewTopStatusBarHost")!;
internal StackPanel GridPreviewTopStatusComponentsPanel => GridSettingsPanel.FindControl<StackPanel>("GridPreviewTopStatusComponentsPanel")!;
internal Border GridPreviewBottomTaskbarContainer => GridSettingsPanel.FindControl<Border>("GridPreviewBottomTaskbarContainer")!;
internal Border GridPreviewTaskbarFixedActionsHost => GridSettingsPanel.FindControl<Border>("GridPreviewTaskbarFixedActionsHost")!;
internal StackPanel GridPreviewBackButtonVisual => GridSettingsPanel.FindControl<StackPanel>("GridPreviewBackButtonVisual")!;
internal TextBlock GridPreviewBackButtonTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPreviewBackButtonTextBlock")!;
internal StackPanel GridPreviewTaskbarDynamicActionsHost => GridSettingsPanel.FindControl<StackPanel>("GridPreviewTaskbarDynamicActionsHost")!;
internal Border GridPreviewTaskbarSettingsActionHost => GridSettingsPanel.FindControl<Border>("GridPreviewTaskbarSettingsActionHost")!;
internal StackPanel GridPreviewComponentLibraryVisual => GridSettingsPanel.FindControl<StackPanel>("GridPreviewComponentLibraryVisual")!;
internal FluentIcons.Avalonia.FluentIcon GridPreviewComponentLibraryIcon => GridSettingsPanel.FindControl<FluentIcons.Avalonia.FluentIcon>("GridPreviewComponentLibraryIcon")!;
internal TextBlock GridPreviewComponentLibraryTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPreviewComponentLibraryTextBlock")!;
internal FluentIcons.Avalonia.SymbolIcon GridPreviewSettingsButtonIcon => GridSettingsPanel.FindControl<FluentIcons.Avalonia.SymbolIcon>("GridPreviewSettingsButtonIcon")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridRowsSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridRowsSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridSpacingSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridSpacingSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridEdgeInsetSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridEdgeInsetSettingsExpander")!;
internal Slider GridSizeSlider => GridSettingsPanel.FindControl<Slider>("GridSizeSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox GridSizeNumberBox => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("GridSizeNumberBox")!;
internal ComboBox GridSpacingPresetComboBox => GridSettingsPanel.FindControl<ComboBox>("GridSpacingPresetComboBox")!;
internal ComboBoxItem GridSpacingRelaxedComboBoxItem => GridSettingsPanel.FindControl<ComboBoxItem>("GridSpacingRelaxedComboBoxItem")!;
internal ComboBoxItem GridSpacingCompactComboBoxItem => GridSettingsPanel.FindControl<ComboBoxItem>("GridSpacingCompactComboBoxItem")!;
internal Slider GridEdgeInsetSlider => GridSettingsPanel.FindControl<Slider>("GridEdgeInsetSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox GridEdgeInsetNumberBox => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("GridEdgeInsetNumberBox")!;
internal TextBlock GridEdgeInsetComputedPxTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridEdgeInsetComputedPxTextBlock")!;
internal Button ApplyGridButton => GridSettingsPanel.FindControl<Button>("ApplyGridButton")!;
internal TextBlock GridInfoTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridInfoTextBlock")!;
// --- ColorSettingsPage ---
internal TextBlock ColorPanelTitleTextBlock => ColorSettingsPanel.FindControl<TextBlock>("ColorPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeModeSettingsExpander => ColorSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeModeSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeColorSettingsExpander => ColorSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeColorSettingsExpander")!;
internal ToggleSwitch NightModeToggleSwitch => ColorSettingsPanel.FindControl<ToggleSwitch>("NightModeToggleSwitch")!;
internal TextBlock ThemeColorStatusTextBlock => ColorSettingsPanel.FindControl<TextBlock>("ThemeColorStatusTextBlock")!;
internal TextBlock RecommendedColorsLabelTextBlock => ColorSettingsPanel.FindControl<TextBlock>("RecommendedColorsLabelTextBlock")!;
internal TextBlock SystemMonetColorsLabelTextBlock => ColorSettingsPanel.FindControl<TextBlock>("SystemMonetColorsLabelTextBlock")!;
internal Button RecommendedColorButton1 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton1")!;
internal Button RecommendedColorButton2 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton2")!;
internal Button RecommendedColorButton3 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton3")!;
internal Button RecommendedColorButton4 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton4")!;
internal Button RecommendedColorButton5 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton5")!;
internal Button RecommendedColorButton6 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton6")!;
internal Border RecommendedColorSwatch1 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch1")!;
internal Border RecommendedColorSwatch2 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch2")!;
internal Border RecommendedColorSwatch3 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch3")!;
internal Border RecommendedColorSwatch4 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch4")!;
internal Border RecommendedColorSwatch5 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch5")!;
internal Border RecommendedColorSwatch6 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch6")!;
internal Button RefreshMonetColorsButton => ColorSettingsPanel.FindControl<Button>("RefreshMonetColorsButton")!;
internal Button MonetColorButton1 => ColorSettingsPanel.FindControl<Button>("MonetColorButton1")!;
internal Button MonetColorButton2 => ColorSettingsPanel.FindControl<Button>("MonetColorButton2")!;
internal Button MonetColorButton3 => ColorSettingsPanel.FindControl<Button>("MonetColorButton3")!;
internal Button MonetColorButton4 => ColorSettingsPanel.FindControl<Button>("MonetColorButton4")!;
internal Button MonetColorButton5 => ColorSettingsPanel.FindControl<Button>("MonetColorButton5")!;
internal Button MonetColorButton6 => ColorSettingsPanel.FindControl<Button>("MonetColorButton6")!;
internal Border MonetColorSwatch1 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch1")!;
internal Border MonetColorSwatch2 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch2")!;
internal Border MonetColorSwatch3 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch3")!;
internal Border MonetColorSwatch4 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch4")!;
internal Border MonetColorSwatch5 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch5")!;
internal Border MonetColorSwatch6 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch6")!;
// --- StatusBarSettingsPage ---
internal TextBlock StatusBarPanelTitleTextBlock => StatusBarSettingsPanel.FindControl<TextBlock>("StatusBarPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarClockSettingsExpander => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarClockSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarSpacingSettingsExpander => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarSpacingSettingsExpander")!;
internal ToggleSwitch StatusBarClockToggleSwitch => StatusBarSettingsPanel.FindControl<ToggleSwitch>("StatusBarClockToggleSwitch")!;
internal RadioButton ClockFormatHMSSRadio => StatusBarSettingsPanel.FindControl<RadioButton>("ClockFormatHMSSRadio")!;
internal RadioButton ClockFormatHMRadio => StatusBarSettingsPanel.FindControl<RadioButton>("ClockFormatHMRadio")!;
internal ComboBox StatusBarSpacingModeComboBox => StatusBarSettingsPanel.FindControl<ComboBox>("StatusBarSpacingModeComboBox")!;
internal ComboBoxItem StatusBarSpacingModeCompactItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeCompactItem")!;
internal ComboBoxItem StatusBarSpacingModeRelaxedItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeRelaxedItem")!;
internal ComboBoxItem StatusBarSpacingModeCustomItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeCustomItem")!;
internal FluentAvalonia.UI.Controls.SettingsExpanderItem StatusBarSpacingCustomPanel => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpanderItem>("StatusBarSpacingCustomPanel")!;
internal Slider StatusBarSpacingSlider => StatusBarSettingsPanel.FindControl<Slider>("StatusBarSpacingSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox StatusBarSpacingNumberBox => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("StatusBarSpacingNumberBox")!;
internal TextBlock StatusBarSpacingComputedPxTextBlock => StatusBarSettingsPanel.FindControl<TextBlock>("StatusBarSpacingComputedPxTextBlock")!;
// --- RegionSettingsPage ---
internal TextBlock RegionPanelTitleTextBlock => RegionSettingsPanel.FindControl<TextBlock>("RegionPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander LanguageSettingsExpander => RegionSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("LanguageSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander TimeZoneSettingsExpander => RegionSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("TimeZoneSettingsExpander")!;
internal ComboBox LanguageComboBox => RegionSettingsPanel.FindControl<ComboBox>("LanguageComboBox")!;
internal ComboBoxItem LanguageChineseItem => RegionSettingsPanel.FindControl<ComboBoxItem>("LanguageChineseItem")!;
internal ComboBoxItem LanguageEnglishItem => RegionSettingsPanel.FindControl<ComboBoxItem>("LanguageEnglishItem")!;
internal ComboBox TimeZoneComboBox => RegionSettingsPanel.FindControl<ComboBox>("TimeZoneComboBox")!;
// --- WeatherSettingsPage ---
internal TextBlock WeatherPanelTitleTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherPreviewSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherPreviewSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherLocationSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherLocationSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCitySearchSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCitySearchSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCoordinateSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCoordinateSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherAlertFilterSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherAlertFilterSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherIconPackSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherIconPackSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherNoTlsSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherNoTlsSettingsExpander")!;
internal Button WeatherPreviewButton => WeatherSettingsPanel.FindControl<Button>("WeatherPreviewButton")!;
internal ComboBox WeatherLocationModeComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherLocationModeComboBox")!;
internal ComboBoxItem WeatherLocationModeCityItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherLocationModeCityItem")!;
internal ComboBoxItem WeatherLocationModeCoordinatesItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherLocationModeCoordinatesItem")!;
internal ListBoxItem WeatherLocationModeCityChipItem => WeatherSettingsPanel.FindControl<ListBoxItem>("WeatherLocationModeCityChipItem")!;
internal ListBoxItem WeatherLocationModeCoordinatesChipItem => WeatherSettingsPanel.FindControl<ListBoxItem>("WeatherLocationModeCoordinatesChipItem")!;
internal ListBox WeatherLocationModeChipListBox => WeatherSettingsPanel.FindControl<ListBox>("WeatherLocationModeChipListBox")!;
internal ToggleSwitch WeatherAutoRefreshToggleSwitch => WeatherSettingsPanel.FindControl<ToggleSwitch>("WeatherAutoRefreshToggleSwitch")!;
internal Button WeatherSearchButton => WeatherSettingsPanel.FindControl<Button>("WeatherSearchButton")!;
internal Button WeatherApplyCityButton => WeatherSettingsPanel.FindControl<Button>("WeatherApplyCityButton")!;
internal Button WeatherApplyCoordinatesButton => WeatherSettingsPanel.FindControl<Button>("WeatherApplyCoordinatesButton")!;
internal TextBox WeatherExcludedAlertsTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherExcludedAlertsTextBox")!;
internal ComboBox WeatherIconPackComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherIconPackComboBox")!;
internal ToggleSwitch WeatherNoTlsToggleSwitch => WeatherSettingsPanel.FindControl<ToggleSwitch>("WeatherNoTlsToggleSwitch")!;
internal TextBox WeatherCitySearchTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherCitySearchTextBox")!;
internal ComboBox WeatherCityResultsComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherCityResultsComboBox")!;
internal TextBlock WeatherSearchStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherSearchStatusTextBlock")!;
internal TextBox WeatherLocationKeyTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherLocationKeyTextBox")!;
internal TextBox WeatherLocationNameTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherLocationNameTextBox")!;
internal FluentAvalonia.UI.Controls.NumberBox WeatherLatitudeNumberBox => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLatitudeNumberBox")!;
internal FluentAvalonia.UI.Controls.NumberBox WeatherLongitudeNumberBox => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLongitudeNumberBox")!;
internal TextBlock WeatherCoordinateStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherCoordinateStatusTextBlock")!;
internal TextBlock WeatherPreviewResultTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewResultTextBlock")!;
internal FluentIcons.Avalonia.Fluent.SymbolIcon WeatherPreviewIconSymbol => WeatherSettingsPanel.FindControl<FluentIcons.Avalonia.Fluent.SymbolIcon>("WeatherPreviewIconSymbol")!;
internal TextBlock WeatherPreviewTemperatureTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewTemperatureTextBlock")!;
internal TextBlock WeatherPreviewUpdatedTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewUpdatedTextBlock")!;
internal FluentAvalonia.UI.Controls.ProgressRing WeatherSearchProgressRing => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherSearchProgressRing")!;
internal FluentAvalonia.UI.Controls.ProgressRing WeatherPreviewProgressRing => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherPreviewProgressRing")!;
internal ComboBoxItem WeatherIconPackFluentRegularItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherIconPackFluentRegularItem")!;
internal ComboBoxItem WeatherIconPackFluentFilledItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherIconPackFluentFilledItem")!;
internal TextBlock WeatherLocationStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherLocationStatusTextBlock")!;
// --- UpdateSettingsPage ---
internal TextBlock UpdatePanelTitleTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateOptionsSettingsExpander => UpdateSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateOptionsSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateActionsSettingsExpander => UpdateSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateActionsSettingsExpander")!;
internal TextBlock UpdateCurrentVersionLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateCurrentVersionLabelTextBlock")!;
internal TextBlock UpdateCurrentVersionValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateCurrentVersionValueTextBlock")!;
internal TextBlock UpdateLatestVersionLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateLatestVersionLabelTextBlock")!;
internal TextBlock UpdateLatestVersionValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateLatestVersionValueTextBlock")!;
internal TextBlock UpdatePublishedAtLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePublishedAtLabelTextBlock")!;
internal TextBlock UpdatePublishedAtValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePublishedAtValueTextBlock")!;
internal TextBlock UpdateChannelLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateChannelLabelTextBlock")!;
internal ListBoxItem UpdateChannelStableChipItem => UpdateSettingsPanel.FindControl<ListBoxItem>("UpdateChannelStableChipItem")!;
internal ListBoxItem UpdateChannelPreviewChipItem => UpdateSettingsPanel.FindControl<ListBoxItem>("UpdateChannelPreviewChipItem")!;
internal ToggleSwitch AutoCheckUpdatesToggleSwitch => UpdateSettingsPanel.FindControl<ToggleSwitch>("AutoCheckUpdatesToggleSwitch")! ;
internal ListBox UpdateChannelChipListBox => UpdateSettingsPanel.FindControl<ListBox>("UpdateChannelChipListBox")!;
internal Button CheckForUpdatesButton => UpdateSettingsPanel.FindControl<Button>("CheckForUpdatesButton")!;
internal Button DownloadAndInstallUpdateButton => UpdateSettingsPanel.FindControl<Button>("DownloadAndInstallUpdateButton")!;
internal ProgressBar UpdateDownloadProgressBar => UpdateSettingsPanel.FindControl<ProgressBar>("UpdateDownloadProgressBar")!;
internal TextBlock UpdateDownloadProgressTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateDownloadProgressTextBlock")!;
internal TextBlock UpdateStatusTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateStatusTextBlock")!;
// --- AboutSettingsPage ---
internal TextBlock AboutPanelTitleTextBlock => AboutSettingsPanel.FindControl<TextBlock>("AboutPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander AboutStartupSettingsExpander => AboutSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutStartupSettingsExpander")!;
internal ToggleSwitch AutoStartWithWindowsToggleSwitch => AboutSettingsPanel.FindControl<ToggleSwitch>("AutoStartWithWindowsToggleSwitch")!;
internal TextBlock VersionTextBlock => AboutSettingsPanel.FindControl<TextBlock>("VersionTextBlock")!;
internal TextBlock CodeNameTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CodeNameTextBlock")!;
internal TextBlock FontInfoTextBlock => AboutSettingsPanel.FindControl<TextBlock>("FontInfoTextBlock")!;
// --- LauncherSettingsPage ---
internal TextBlock LauncherSettingsPanelTitleTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherSettingsPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander LauncherHiddenItemsSettingsExpander => LauncherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("LauncherHiddenItemsSettingsExpander")!;
internal StackPanel LauncherHiddenItemsListPanel => LauncherSettingsPanel.FindControl<StackPanel>("LauncherHiddenItemsListPanel")!;
internal TextBlock LauncherHiddenItemsEmptyTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherHiddenItemsEmptyTextBlock")!;
internal TextBlock LauncherHiddenItemsDescriptionTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherHiddenItemsDescriptionTextBlock")!;
// --- PluginSettingsPage (Added for completeness) ---
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 TextBlock PluginRestartHintTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginRestartHintTextBlock")!;
internal TextBlock PluginCatalogEmptyTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginCatalogEmptyTextBlock")!;
}

View File

@@ -0,0 +1,500 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
protected override void OnClosed(EventArgs e)
{
_persistSettingsDebounceTimer?.Dispose();
_persistSettingsDebounceTimer = null;
StopVideoWallpaper();
_previewVideoWallpaperPlayer?.Dispose();
_previewVideoWallpaperPlayer = null;
_previewVideoWallpaperMedia?.Dispose();
_previewVideoWallpaperMedia = null;
_libVlc?.Dispose();
_libVlc = null;
_releaseUpdateService.Dispose();
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
_launcherFolderIconBitmap?.Dispose();
_launcherFolderIconBitmap = null;
foreach (var icon in _launcherIconCache.Values)
{
icon.Dispose();
}
_launcherIconCache.Clear();
base.OnClosed(e);
}
private void OnSettingsNavSelectionChanged(object? sender, FluentAvalonia.UI.Controls.NavigationViewSelectionChangedEventArgs e)
{
UpdateSettingsTabContent();
PersistSettings();
}
private int GetSettingsTabIndex()
{
return ResolveSelectedSettingsTabIndex();
}
private void UpdateSettingsTabContent()
{
if (SettingsNavView is null)
{
return;
}
var selectedItem = SettingsNavView.SelectedItem as FluentAvalonia.UI.Controls.NavigationViewItem;
var tag = selectedItem?.Tag?.ToString();
WallpaperSettingsPanel.IsVisible = tag == "Wallpaper";
GridSettingsPanel.IsVisible = tag == "Grid";
ColorSettingsPanel.IsVisible = tag == "Color";
StatusBarSettingsPanel.IsVisible = tag == "StatusBar";
WeatherSettingsPanel.IsVisible = tag == "Weather";
RegionSettingsPanel.IsVisible = tag == "Region";
UpdateSettingsPanel.IsVisible = tag == "Update";
AboutSettingsPanel.IsVisible = tag == "About";
LauncherSettingsPanel.IsVisible = tag == "Launcher";
PluginSettingsPanel.IsVisible = tag == "Plugins";
UpdatePluginSettingsPageVisibility(tag);
if (tag == "Launcher")
{
RenderLauncherHiddenItemsList();
}
if (tag == "Grid")
{
UpdateGridPreviewLayout();
}
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
private void PersistSettings()
{
if (_suppressSettingsPersistence)
{
return;
}
_appSettingsService.Save(BuildAppSettingsSnapshot());
_launcherSettingsService.Save(BuildLauncherSettingsSnapshot());
}
private AppSettingsSnapshot BuildAppSettingsSnapshot()
{
var snapshot = _appSettingsService.Load();
snapshot.GridShortSideCells = _targetShortSideCells;
snapshot.GridSpacingPreset = _gridSpacingPreset;
snapshot.DesktopEdgeInsetPercent = _desktopEdgeInsetPercent;
snapshot.IsNightMode = _isNightMode;
snapshot.ThemeColor = _selectedThemeColor.ToString();
snapshot.WallpaperPath = _wallpaperPath;
snapshot.WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement());
snapshot.SettingsTabIndex = Math.Max(0, GetSettingsTabIndex());
snapshot.SettingsTabTag = GetSelectedSettingsTabTag();
snapshot.LanguageCode = _languageCode;
snapshot.TimeZoneId = _timeZoneService.CurrentTimeZone.Id;
snapshot.WeatherLocationMode = ToWeatherLocationModeTag(_weatherLocationMode);
snapshot.WeatherLocationKey = _weatherLocationKey;
snapshot.WeatherLocationName = _weatherLocationName;
snapshot.WeatherLatitude = _weatherLatitude;
snapshot.WeatherLongitude = _weatherLongitude;
snapshot.WeatherAutoRefreshLocation = _weatherAutoRefreshLocation;
snapshot.WeatherLocationQuery = BuildLegacyWeatherLocationQuery();
snapshot.WeatherExcludedAlerts = _weatherExcludedAlertsRaw;
snapshot.WeatherIconPackId = _weatherIconPackId;
snapshot.WeatherNoTlsRequests = _weatherNoTlsRequests;
snapshot.AutoStartWithWindows = _autoStartWithWindows;
snapshot.AutoCheckUpdates = _autoCheckUpdates;
snapshot.IncludePrereleaseUpdates = IncludePrereleaseUpdates;
snapshot.UpdateChannel = IncludePrereleaseUpdates ? UpdateChannelPreview : UpdateChannelStable;
snapshot.TopStatusComponentIds = _topStatusComponentIds.ToList();
snapshot.PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList();
snapshot.EnableDynamicTaskbarActions = _enableDynamicTaskbarActions;
snapshot.TaskbarLayoutMode = _taskbarLayoutMode;
snapshot.ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond";
snapshot.StatusBarSpacingMode = _statusBarSpacingMode;
snapshot.StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent;
return snapshot;
}
private LauncherSettingsSnapshot BuildLauncherSettingsSnapshot()
{
return new LauncherSettingsSnapshot
{
HiddenLauncherFolderPaths = _hiddenLauncherFolderPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToList(),
HiddenLauncherAppPaths = _hiddenLauncherAppPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToList()
};
}
private void SchedulePersistSettings(int delayMs = 200)
{
if (_suppressSettingsPersistence)
{
return;
}
_persistSettingsDebounceTimer?.Dispose();
_persistSettingsDebounceTimer = DispatcherTimer.RunOnce(() =>
{
_persistSettingsDebounceTimer = null;
PersistSettings();
}, TimeSpan.FromMilliseconds(Math.Max(0, delayMs)));
}
private int CalculateDefaultShortSideCellCountFromDpi()
{
var dpi = 96d * RenderScaling;
var count = (int)Math.Round(dpi / 8d);
return Math.Clamp(count, MinShortSideCells, MaxShortSideCells);
}
private static string NormalizeGridSpacingPreset(string? value)
{
return string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase)
? "Compact"
: "Relaxed";
}
private static string NormalizeStatusBarSpacingMode(string? value)
{
return value switch
{
_ when string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase) => "Compact",
_ when string.Equals(value, "Custom", StringComparison.OrdinalIgnoreCase) => "Custom",
_ => "Relaxed"
};
}
private static string? TryGetSelectedComboBoxTag(ComboBox? comboBox)
{
if (comboBox?.SelectedItem is ComboBoxItem item)
{
return item.Tag?.ToString();
}
return comboBox?.SelectedItem?.ToString();
}
private int ResolveStatusBarSpacingPercent()
{
return _statusBarSpacingMode switch
{
"Compact" => 6,
"Custom" => Math.Clamp(_statusBarCustomSpacingPercent, 0, 30),
_ => 12
};
}
private void ApplyStatusBarComponentSpacingForPanel(StackPanel? panel, double cellSize)
{
if (panel is null)
{
return;
}
var percent = ResolveStatusBarSpacingPercent();
panel.Spacing = Math.Max(0, cellSize) * (percent / 100d);
}
private void UpdateStatusBarSpacingComputedPxText(double cellSize)
{
var percent = ResolveStatusBarSpacingPercent();
var spacingPx = Math.Max(0, cellSize) * (percent / 100d);
StatusBarSpacingComputedPxTextBlock.Text = Lf(
"settings.status_bar.spacing_custom_px_format",
">= {0:F1}px",
spacingPx);
}
private int ResolvePendingGridEdgeInsetPercent()
{
var pending = (int)Math.Round(GridEdgeInsetNumberBox.Value);
return Math.Clamp(pending, MinEdgeInsetPercent, MaxEdgeInsetPercent);
}
private void UpdateGridEdgeInsetComputedPxText(double cellSize)
{
var percent = ResolvePendingGridEdgeInsetPercent();
var insetPx = Math.Clamp(Math.Max(0, cellSize) * (percent / 100d), 0, 80);
GridEdgeInsetComputedPxTextBlock.Text = Lf("settings.grid.edge_inset_px_format", "{0:F1}px", insetPx);
}
private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
{
return;
}
_topStatusComponentIds.Add(BuiltInComponentIds.Clock);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void OnStatusBarClockUnchecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
{
return;
}
_topStatusComponentIds.Remove(BuiltInComponentIds.Clock);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void OnClockFormatChanged(object? sender, RoutedEventArgs e)
{
if (sender is not RadioButton radioButton || radioButton.Tag is not string formatTag)
{
return;
}
_clockDisplayFormat = formatTag == "Hm"
? ClockDisplayFormat.HourMinute
: ClockDisplayFormat.HourMinuteSecond;
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void ApplyTaskbarSettings(AppSettingsSnapshot snapshot)
{
_topStatusComponentIds.Clear();
if (snapshot.TopStatusComponentIds is not null)
{
foreach (var componentId in snapshot.TopStatusComponentIds)
{
if (string.IsNullOrWhiteSpace(componentId))
{
continue;
}
var normalizedId = componentId.Trim();
if (_componentRegistry.IsKnownComponent(normalizedId) &&
_componentRegistry.AllowsStatusBarPlacement(normalizedId))
{
_topStatusComponentIds.Add(normalizedId);
}
}
}
_pinnedTaskbarActions.Clear();
if (snapshot.PinnedTaskbarActions is not null)
{
foreach (var actionText in snapshot.PinnedTaskbarActions)
{
if (Enum.TryParse<TaskbarActionId>(actionText, ignoreCase: true, out var action))
{
_pinnedTaskbarActions.Add(action);
}
}
}
if (_pinnedTaskbarActions.Count == 0)
{
foreach (var action in DefaultPinnedTaskbarActions)
{
_pinnedTaskbarActions.Add(action);
}
}
_enableDynamicTaskbarActions = snapshot.EnableDynamicTaskbarActions;
_taskbarLayoutMode = string.IsNullOrWhiteSpace(snapshot.TaskbarLayoutMode)
? TaskbarLayoutBottomFullRowMacStyle
: snapshot.TaskbarLayoutMode;
_clockDisplayFormat = snapshot.ClockDisplayFormat == "HourMinute"
? ClockDisplayFormat.HourMinute
: ClockDisplayFormat.HourMinuteSecond;
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
if (_clockDisplayFormat == ClockDisplayFormat.HourMinute)
{
ClockFormatHMRadio.IsChecked = true;
}
else
{
ClockFormatHMSSRadio.IsChecked = true;
}
_suppressStatusBarToggleEvents = true;
StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
_suppressStatusBarToggleEvents = false;
}
private void ApplyTopStatusComponentVisibility()
{
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
ClockWidget.IsVisible = showClock;
if (showClock)
{
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
Grid.SetColumnSpan(ClockWidget, _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3);
}
WallpaperPreviewClockWidget.IsVisible = showClock;
if (showClock)
{
WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
}
}
private TaskbarContext GetCurrentTaskbarContext()
{
var selectedItem = SettingsNavView?.SelectedItem as FluentAvalonia.UI.Controls.NavigationViewItem;
return selectedItem?.Tag?.ToString() switch
{
"Wallpaper" => TaskbarContext.SettingsWallpaper,
"Grid" => TaskbarContext.SettingsGrid,
"Color" => TaskbarContext.SettingsColor,
"StatusBar" => TaskbarContext.SettingsStatusBar,
"Weather" => TaskbarContext.SettingsWeather,
"Region" => TaskbarContext.SettingsRegion,
_ => TaskbarContext.Desktop
};
}
private void ApplyTaskbarActionVisibility(TaskbarContext context)
{
_ = context;
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings);
BackToWindowsButton.IsVisible = showMinimize;
OpenComponentLibraryButton.IsVisible = false;
OpenSettingsButton.IsVisible = showSettings;
WallpaperPreviewBackButtonVisual.IsVisible = showMinimize;
WallpaperPreviewComponentLibraryVisual.IsVisible = false;
WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings;
TaskbarFixedActionsHost.IsVisible = showMinimize;
TaskbarSettingsActionHost.IsVisible = showSettings;
TaskbarDynamicActionsHost.IsVisible = false;
WallpaperPreviewTaskbarFixedActionsHost.IsVisible = showMinimize;
WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings;
WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = false;
UpdateOpenSettingsActionVisualState();
}
private void UpdateOpenSettingsActionVisualState()
{
OpenSettingsButtonTextBlock.IsVisible = false;
OpenSettingsButtonTextBlock.Text = L("tooltip.open_settings", "Settings");
}
private void InitializeSettingsIcons()
{
const IconVariant variant = IconVariant.Regular;
WallpaperPlacementSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Wallpaper, IconVariant = variant };
ThemeColorSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Color, IconVariant = variant };
StatusBarClockSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Clock, IconVariant = variant };
StatusBarSpacingSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.TextLineSpacing, IconVariant = variant };
WeatherLocationSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.WeatherSunny, IconVariant = variant };
WeatherPreviewSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.WeatherSunny, IconVariant = variant };
WeatherAlertFilterSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Info, IconVariant = variant };
WeatherIconPackSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Color, IconVariant = variant };
WeatherNoTlsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Globe, IconVariant = variant };
LanguageSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Translate, IconVariant = variant };
TimeZoneSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.GlobeClock, IconVariant = variant };
UpdateOptionsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.ArrowClockwiseDashesSettings, IconVariant = variant };
UpdateActionsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.ArrowDownload, IconVariant = variant };
AboutStartupSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Play, IconVariant = variant };
PluginSystemSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.PuzzlePiece, IconVariant = variant };
UpdateThemeModeIcon();
}
private void UpdateThemeModeIcon()
{
ThemeModeSettingsExpander.IconSource = new SymbolIconSource
{
Symbol = _isNightMode ? Symbol.WeatherMoon : Symbol.WeatherSunny,
IconVariant = IconVariant.Regular
};
}
private void InitializeTimeZoneSettings()
{
_suppressTimeZoneSelectionEvents = true;
TimeZoneComboBox.Items.Clear();
foreach (var tz in _timeZoneService.GetAllTimeZones())
{
var item = new ComboBoxItem
{
Content = GetLocalizedTimeZoneDisplayName(tz),
Tag = tz.Id
};
TimeZoneComboBox.Items.Add(item);
if (tz.Id == _timeZoneService.CurrentTimeZone.Id)
{
TimeZoneComboBox.SelectedItem = item;
}
}
ClockWidget.SetTimeZoneService(_timeZoneService);
WallpaperPreviewClockWidget.SetTimeZoneService(_timeZoneService);
_suppressTimeZoneSelectionEvents = false;
}
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressTimeZoneSelectionEvents || TimeZoneComboBox.SelectedItem is not ComboBoxItem item)
{
return;
}
var timeZoneId = item.Tag?.ToString();
if (string.IsNullOrWhiteSpace(timeZoneId))
{
return;
}
_timeZoneService.SetTimeZoneById(timeZoneId);
PersistSettings();
}
private IBrush GetThemeBrush(string key)
{
if (Resources.TryGetResource(key, ActualThemeVariant, out var resource) && resource is IBrush brush)
{
return brush;
}
return Brushes.Transparent;
}
}

View File

@@ -0,0 +1,529 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using Line = Avalonia.Controls.Shapes.Line;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void OnGridSizeSliderChanged(object? sender, RoutedEventArgs e)
{
var sliderValue = (int)Math.Round(GridSizeSlider.Value);
if (Math.Abs(GridSizeNumberBox.Value - sliderValue) > double.Epsilon)
{
GridSizeNumberBox.Value = sliderValue;
}
UpdateGridPreviewLayout();
}
private void OnGridSizeNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
{
var numberBoxValue = (int)Math.Round(GridSizeNumberBox.Value);
if (Math.Abs(GridSizeSlider.Value - numberBoxValue) > double.Epsilon)
{
GridSizeSlider.Value = numberBoxValue;
}
UpdateGridPreviewLayout();
}
private void OnGridEdgeInsetSliderChanged(object? sender, RoutedEventArgs e)
{
if (_suppressGridInsetEvents)
{
return;
}
var value = (int)Math.Round(GridEdgeInsetSlider.Value);
SetPendingGridEdgeInsetPercent(value, updateSlider: false, updateNumberBox: true);
UpdateGridPreviewLayout();
}
private void OnGridEdgeInsetNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
{
if (_suppressGridInsetEvents)
{
return;
}
var value = (int)Math.Round(GridEdgeInsetNumberBox.Value);
SetPendingGridEdgeInsetPercent(value, updateSlider: true, updateNumberBox: false);
UpdateGridPreviewLayout();
}
private void SetPendingGridEdgeInsetPercent(int percent, bool updateSlider, bool updateNumberBox)
{
var clamped = Math.Clamp(percent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_suppressGridInsetEvents = true;
try
{
if (updateSlider && Math.Abs(GridEdgeInsetSlider.Value - clamped) > double.Epsilon)
{
GridEdgeInsetSlider.Value = clamped;
}
if (updateNumberBox && Math.Abs(GridEdgeInsetNumberBox.Value - clamped) > double.Epsilon)
{
GridEdgeInsetNumberBox.Value = clamped;
}
}
finally
{
_suppressGridInsetEvents = false;
}
}
private void OnGridSpacingPresetSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressGridSpacingEvents)
{
return;
}
UpdateGridPreviewLayout();
}
private void OnStatusBarSpacingModeChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressStatusBarSpacingEvents)
{
return;
}
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(
TryGetSelectedComboBoxTag(StatusBarSpacingModeComboBox) ?? _statusBarSpacingMode);
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
SchedulePersistSettings();
}
private void OnStatusBarSpacingSliderChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
if (_suppressStatusBarSpacingEvents)
{
return;
}
var percent = (int)Math.Round(StatusBarSpacingSlider.Value);
SetStatusBarCustomSpacingPercent(percent, updateSlider: false, updateNumberBox: true);
if (string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase))
{
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
}
SchedulePersistSettings();
}
private void OnStatusBarSpacingNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
{
if (_suppressStatusBarSpacingEvents)
{
return;
}
var percent = (int)Math.Round(StatusBarSpacingNumberBox.Value);
SetStatusBarCustomSpacingPercent(percent, updateSlider: true, updateNumberBox: false);
if (string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase))
{
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
}
SchedulePersistSettings();
}
private void SetStatusBarCustomSpacingPercent(int percent, bool updateSlider, bool updateNumberBox)
{
percent = Math.Clamp(percent, 0, 30);
_statusBarCustomSpacingPercent = percent;
_suppressStatusBarSpacingEvents = true;
try
{
if (updateSlider && Math.Abs(StatusBarSpacingSlider.Value - percent) > double.Epsilon)
{
StatusBarSpacingSlider.Value = percent;
}
if (updateNumberBox && Math.Abs(StatusBarSpacingNumberBox.Value - percent) > double.Epsilon)
{
StatusBarSpacingNumberBox.Value = percent;
}
}
finally
{
_suppressStatusBarSpacingEvents = false;
}
}
private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e)
{
_gridSpacingPreset = NormalizeGridSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
_desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
var requested = (int)Math.Round(GridSizeNumberBox.Value);
if (requested <= 0)
{
requested = _targetShortSideCells;
}
_targetShortSideCells = Math.Clamp(requested, MinShortSideCells, MaxShortSideCells);
if (Math.Abs(GridSizeNumberBox.Value - _targetShortSideCells) > double.Epsilon)
{
GridSizeNumberBox.Value = _targetShortSideCells;
}
if (Math.Abs(GridSizeSlider.Value - _targetShortSideCells) > double.Epsilon)
{
GridSizeSlider.Value = _targetShortSideCells;
}
SetPendingGridEdgeInsetPercent(_desktopEdgeInsetPercent, updateSlider: true, updateNumberBox: true);
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
PersistSettings();
}
private static double ResolveGridGapRatio(string preset)
{
return string.Equals(preset, "Compact", StringComparison.OrdinalIgnoreCase) ? 0.06 : 0.12;
}
private static double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent)
{
if (hostWidth <= 1 || hostHeight <= 1)
{
return 0;
}
var cells = Math.Max(1, shortSideCells);
var shortSidePx = Math.Max(1, Math.Min(hostWidth, hostHeight));
var baseCell = shortSidePx / cells;
return Math.Clamp(baseCell * (Math.Clamp(insetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent) / 100d), 0, 80);
}
private static GridMetrics CalculateGridMetrics(double hostWidth, double hostHeight, int shortSideCells, double gapRatio, double edgeInsetPx)
{
if (hostWidth <= 1 || hostHeight <= 1)
{
return default;
}
var shortSide = Math.Max(1, shortSideCells);
var clampedGapRatio = Math.Max(0, gapRatio);
var inset = Math.Max(0, edgeInsetPx);
var availableWidth = Math.Max(1, hostWidth - inset * 2);
var availableHeight = Math.Max(1, hostHeight - inset * 2);
if (hostWidth >= hostHeight)
{
var rowCount = shortSide;
var denominator = rowCount + Math.Max(0, rowCount - 1) * clampedGapRatio;
if (denominator <= 0)
{
return default;
}
var cellSize = availableHeight / denominator;
var gapPx = cellSize * clampedGapRatio;
var pitch = cellSize + gapPx;
if (pitch <= 0)
{
return default;
}
var columnCount = Math.Max(1, (int)Math.Floor((availableWidth + gapPx) / pitch));
var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx;
var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx;
return new GridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight);
}
var columnCountPortrait = shortSide;
var denominatorPortrait = columnCountPortrait + Math.Max(0, columnCountPortrait - 1) * clampedGapRatio;
if (denominatorPortrait <= 0)
{
return default;
}
var cellSizePortrait = availableWidth / denominatorPortrait;
var gapPxPortrait = cellSizePortrait * clampedGapRatio;
var pitchPortrait = cellSizePortrait + gapPxPortrait;
if (pitchPortrait <= 0)
{
return default;
}
var rowCountPortrait = Math.Max(1, (int)Math.Floor((availableHeight + gapPxPortrait) / pitchPortrait));
var gridWidthPortrait = columnCountPortrait * cellSizePortrait + Math.Max(0, columnCountPortrait - 1) * gapPxPortrait;
var gridHeightPortrait = rowCountPortrait * cellSizePortrait + Math.Max(0, rowCountPortrait - 1) * gapPxPortrait;
return new GridMetrics(columnCountPortrait, rowCountPortrait, cellSizePortrait, gapPxPortrait, inset, gridWidthPortrait, gridHeightPortrait);
}
private static int ClampComponentSpan(int requestedSpan, int axisCellCount)
{
return Math.Clamp(requestedSpan, 1, Math.Max(1, axisCellCount));
}
private static int ClampGridIndex(int requestedIndex, int axisCellCount)
{
return Math.Clamp(requestedIndex, 0, Math.Max(0, axisCellCount - 1));
}
private static void PlaceStatusBarComponent(Control component, int column, int requestedColumnSpan, int totalColumns)
{
var clampedColumn = ClampGridIndex(column, totalColumns);
var availableColumns = Math.Max(1, totalColumns - clampedColumn);
Grid.SetRow(component, StatusBarRowIndex);
Grid.SetColumn(component, clampedColumn);
Grid.SetColumnSpan(component, ClampComponentSpan(requestedColumnSpan, availableColumns));
}
private void UpdateGridPreviewLayout()
{
var previewShortSideCells = (int)Math.Round(GridSizeSlider.Value);
if (previewShortSideCells < MinShortSideCells || previewShortSideCells > MaxShortSideCells)
{
previewShortSideCells = _targetShortSideCells;
}
var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width > 1 ? DesktopHost.Bounds.Width : Bounds.Width);
var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height > 1 ? DesktopHost.Bounds.Height : Bounds.Height);
var aspectRatio = desktopWidth / desktopHeight;
var availableWidth = Math.Max(100, GridPreviewHost.Bounds.Width);
var framePadding = GridPreviewFrame.Padding;
var horizontalPadding = framePadding.Left + framePadding.Right;
var verticalPadding = framePadding.Top + framePadding.Bottom;
var gridPreviewWidth = availableWidth;
var gridPreviewHeight = gridPreviewWidth / aspectRatio;
GridPreviewFrame.Width = gridPreviewWidth;
GridPreviewFrame.Height = gridPreviewHeight;
var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding);
var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding);
var preset = NormalizeGridSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
var gapRatio = ResolveGridGapRatio(preset);
var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, ResolvePendingGridEdgeInsetPercent());
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0)
{
return;
}
_currentDesktopCellSize = gridMetrics.CellSize;
GridPreviewGrid.Margin = new Thickness(gridMetrics.EdgeInsetPx);
GridPreviewGrid.RowSpacing = gridMetrics.GapPx;
GridPreviewGrid.ColumnSpacing = gridMetrics.GapPx;
GridPreviewGrid.Width = gridMetrics.GridWidthPx;
GridPreviewGrid.Height = gridMetrics.GridHeightPx;
GridPreviewLinesCanvas.Margin = new Thickness(gridMetrics.EdgeInsetPx);
GridPreviewGrid.RowDefinitions.Clear();
GridPreviewGrid.ColumnDefinitions.Clear();
for (var row = 0; row < gridMetrics.RowCount; row++)
{
GridPreviewGrid.RowDefinitions.Add(new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel)));
}
for (var col = 0; col < gridMetrics.ColumnCount; col++)
{
GridPreviewGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel)));
}
PlaceStatusBarComponent(GridPreviewTopStatusBarHost, 0, gridMetrics.ColumnCount, gridMetrics.ColumnCount);
var taskbarRow = gridMetrics.RowCount - 1;
Grid.SetRow(GridPreviewBottomTaskbarContainer, taskbarRow);
Grid.SetColumn(GridPreviewBottomTaskbarContainer, 0);
Grid.SetColumnSpan(GridPreviewBottomTaskbarContainer, gridMetrics.ColumnCount);
ApplyGridPreviewWidgetSizing(gridMetrics.CellSize);
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);
DrawGridPreviewLines(gridMetrics);
}
private void DrawGridPreviewLines(GridMetrics gridMetrics)
{
var viewportBackground = GridPreviewViewport.Background as SolidColorBrush;
var backgroundColor = viewportBackground?.Color ?? Color.Parse("#30111827");
var luminance = CalculateRelativeLuminance(backgroundColor);
var lineColor = luminance >= LightBackgroundLuminanceThreshold ? Color.Parse("#80000000") : Color.Parse("#80FFFFFF");
GridPreviewLinesCanvas.Children.Clear();
GridPreviewLinesCanvas.Width = gridMetrics.GridWidthPx;
GridPreviewLinesCanvas.Height = gridMetrics.GridHeightPx;
var dashLength = gridMetrics.CellSize * 0.3;
var gapLength = gridMetrics.CellSize * 0.2;
for (var row = 0; row <= gridMetrics.RowCount; row++)
{
var y = row == gridMetrics.RowCount ? gridMetrics.GridHeightPx : row * gridMetrics.Pitch;
GridPreviewLinesCanvas.Children.Add(new Line
{
StartPoint = new Point(0, y),
EndPoint = new Point(gridMetrics.GridWidthPx, y),
Stroke = new SolidColorBrush(lineColor),
StrokeThickness = 1,
StrokeDashArray = new Avalonia.Collections.AvaloniaList<double> { dashLength, gapLength },
IsHitTestVisible = false
});
}
for (var col = 0; col <= gridMetrics.ColumnCount; col++)
{
var x = col == gridMetrics.ColumnCount ? gridMetrics.GridWidthPx : col * gridMetrics.Pitch;
GridPreviewLinesCanvas.Children.Add(new Line
{
StartPoint = new Point(x, 0),
EndPoint = new Point(x, gridMetrics.GridHeightPx),
Stroke = new SolidColorBrush(lineColor),
StrokeThickness = 1,
StrokeDashArray = new Avalonia.Collections.AvaloniaList<double> { dashLength, gapLength },
IsHitTestVisible = false
});
}
}
private void ApplyGridPreviewWidgetSizing(double cellSize)
{
var previewTaskbarCell = Math.Clamp(cellSize * 0.74, 10, 30);
var iconSize = Math.Clamp(cellSize * 0.35, 8, 16);
GridPreviewTopStatusBarHost.Padding = new Thickness(0);
GridPreviewBottomTaskbarContainer.Margin = new Thickness(0);
GridPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 16, 32));
GridPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4));
GridPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13);
GridPreviewComponentLibraryTextBlock.FontSize = Math.Clamp(cellSize * 0.18, 5, 12);
GridPreviewComponentLibraryIcon.FontSize = iconSize;
GridPreviewBackButtonVisual.MinHeight = previewTaskbarCell;
GridPreviewBackButtonVisual.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120);
GridPreviewComponentLibraryVisual.MinHeight = previewTaskbarCell;
GridPreviewComponentLibraryVisual.MinWidth = Math.Clamp(cellSize * 2.0, 28, 110);
GridPreviewSettingsButtonIcon.Width = Math.Clamp(previewTaskbarCell * 0.42, 6, 14);
GridPreviewSettingsButtonIcon.Height = Math.Clamp(previewTaskbarCell * 0.42, 6, 14);
}
private void UpdateWallpaperPreviewLayout()
{
if (_isUpdatingWallpaperPreviewLayout)
{
return;
}
_isUpdatingWallpaperPreviewLayout = true;
try
{
var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width > 1 ? DesktopHost.Bounds.Width : Bounds.Width);
var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height > 1 ? DesktopHost.Bounds.Height : Bounds.Height);
var aspectRatio = desktopWidth / desktopHeight;
var availableWidth = Math.Max(100, WallpaperPreviewHost.Bounds.Width);
var availableHeight = WallpaperPreviewHost.Bounds.Height < 120 ? double.PositiveInfinity : WallpaperPreviewHost.Bounds.Height;
var framePadding = WallpaperPreviewFrame.Padding;
var horizontalPadding = framePadding.Left + framePadding.Right;
var verticalPadding = framePadding.Top + framePadding.Bottom;
var previewWidth = Math.Min(availableWidth, WallpaperPreviewMaxWidth);
var previewHeight = previewWidth / aspectRatio;
if (double.IsFinite(availableHeight) && previewHeight > availableHeight)
{
previewHeight = availableHeight;
previewWidth = previewHeight * aspectRatio;
}
WallpaperPreviewFrame.Width = previewWidth;
WallpaperPreviewFrame.Height = previewHeight;
var innerWidth = Math.Max(1, previewWidth - horizontalPadding);
var innerHeight = Math.Max(1, previewHeight - verticalPadding);
var gapRatio = ResolveGridGapRatio(_gridSpacingPreset);
var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0)
{
return;
}
WallpaperPreviewGrid.Margin = new Thickness(gridMetrics.EdgeInsetPx);
WallpaperPreviewGrid.RowSpacing = gridMetrics.GapPx;
WallpaperPreviewGrid.ColumnSpacing = gridMetrics.GapPx;
WallpaperPreviewGrid.Width = gridMetrics.GridWidthPx;
WallpaperPreviewGrid.Height = gridMetrics.GridHeightPx;
WallpaperPreviewGrid.RowDefinitions.Clear();
WallpaperPreviewGrid.ColumnDefinitions.Clear();
for (var row = 0; row < gridMetrics.RowCount; row++)
{
WallpaperPreviewGrid.RowDefinitions.Add(new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel)));
}
for (var col = 0; col < gridMetrics.ColumnCount; col++)
{
WallpaperPreviewGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel)));
}
PlaceStatusBarComponent(WallpaperPreviewTopStatusBarHost, 0, gridMetrics.ColumnCount, gridMetrics.ColumnCount);
var taskbarRow = gridMetrics.RowCount - 1;
Grid.SetRow(WallpaperPreviewBottomTaskbarContainer, taskbarRow);
Grid.SetColumn(WallpaperPreviewBottomTaskbarContainer, 0);
Grid.SetColumnSpan(WallpaperPreviewBottomTaskbarContainer, gridMetrics.ColumnCount);
ApplyTopStatusComponentVisibility();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
ApplyPreviewWidgetSizing(gridMetrics.CellSize);
ApplyStatusBarComponentSpacingForPanel(WallpaperPreviewTopStatusComponentsPanel, gridMetrics.CellSize);
}
finally
{
_isUpdatingWallpaperPreviewLayout = false;
}
}
private void ApplyPreviewWidgetSizing(double cellSize)
{
var previewTaskbarCell = Math.Clamp(cellSize * 0.74, 10, 28);
var previewTextSize = Math.Clamp(previewTaskbarCell * 0.38, 7, 14);
var previewIconSize = Math.Clamp(previewTaskbarCell * 0.46, 8, 16);
var previewInset = Math.Clamp(previewTaskbarCell * 0.20, 2, 6);
var previewContentSpacing = Math.Clamp(previewTaskbarCell * 0.20, 2, 6);
WallpaperPreviewTopStatusBarHost.Margin = new Thickness(0);
WallpaperPreviewTopStatusBarHost.Padding = new Thickness(0);
WallpaperPreviewBottomTaskbarContainer.Margin = new Thickness(0);
WallpaperPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 6, 14));
WallpaperPreviewBottomTaskbarContainer.Padding = new Thickness(previewInset);
WallpaperPreviewClockWidget.ApplyCellSize(cellSize);
WallpaperPreviewBackButtonTextBlock.FontSize = previewTextSize;
WallpaperPreviewComponentLibraryTextBlock.FontSize = previewTextSize;
WallpaperPreviewBackButtonVisual.Spacing = previewContentSpacing;
WallpaperPreviewComponentLibraryVisual.Spacing = previewContentSpacing;
WallpaperPreviewBackButtonVisual.MinHeight = previewTaskbarCell;
WallpaperPreviewBackButtonVisual.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120);
WallpaperPreviewComponentLibraryVisual.MinHeight = previewTaskbarCell;
WallpaperPreviewComponentLibraryVisual.MinWidth = Math.Clamp(cellSize * 2.0, 28, 110);
WallpaperPreviewSettingsButtonIcon.Width = previewIconSize;
WallpaperPreviewSettingsButtonIcon.Height = previewIconSize;
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.Linq;
using Avalonia.Controls;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void InitializeLocalization(string? languageCode)
{
_languageCode = _localizationService.NormalizeLanguageCode(languageCode);
_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("settings.title", "Settings");
WindowTitleTextBlock.Text = L("settings.title", "Settings");
WindowSubtitleTextBlock.Text = L("settings.footer", "LanMountainDesktop Settings");
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");
SettingsNavAboutItem.Content = L("settings.nav.about", "About");
SettingsNavLauncherItem.Content = L("settings.nav.launcher", "App Launcher");
SettingsNavPluginsItem.Content = L("settings.nav.plugins", "Plugins");
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");
WallpaperPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows");
WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
GridPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows");
GridPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
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");
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 (%)");
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.");
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.");
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", "Enable or disable plugins here. Detailed plugin settings appear as separate settings pages.");
PluginRestartHintTextBlock.Text = L("settings.plugins.restart_hint", "Plugin enable state changes take effect after restarting the app.");
PluginCatalogEmptyTextBlock.Text = L("settings.plugins.empty", "No plugins found.");
PluginSettingsPanel.RefreshFromRuntime();
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.");
var placementItems = WallpaperPlacementComboBox.Items.OfType<ComboBoxItem>().ToList();
if (placementItems.Count >= 5)
{
placementItems[0].Content = L("placement.fill", "Fill");
placementItems[1].Content = L("placement.fit", "Fit");
placementItems[2].Content = L("placement.stretch", "Stretch");
placementItems[3].Content = L("placement.center", "Center");
placementItems[4].Content = L("placement.tile", "Tile");
}
ApplyUpdateLocalization();
UpdateWallpaperDisplay();
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.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;
}
_languageCode = _localizationService.NormalizeLanguageCode(item.Tag as string);
ApplyLocalization();
ThemeColorStatusTextBlock.Text = Lf("settings.region.applied_format", "Language switched to: {0}", GetLanguageDisplayName(_languageCode));
PersistSettings();
}
}

View File

@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private readonly Dictionary<string, Control> _pluginSettingsPageHosts = new(StringComparer.OrdinalIgnoreCase);
private void InitializePluginSettingsNavigation()
{
if (_pluginSettingsPageHosts.Count > 0 || SettingsNavView?.MenuItems is null)
{
return;
}
var runtime = (Application.Current as App)?.PluginRuntimeService;
var contributions = runtime?.SettingsPages
.OrderBy(contribution => contribution.Registration.SortOrder)
.ThenBy(contribution => contribution.Plugin.Manifest.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(contribution => contribution.Registration.Title, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (contributions is not { Length: > 0 })
{
return;
}
var pageCountsByPluginId = contributions
.GroupBy(contribution => contribution.Plugin.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
var insertIndex = SettingsNavView.MenuItems.IndexOf(SettingsNavPluginsItem) + 1;
foreach (var contribution in contributions)
{
var tag = BuildPluginSettingsTag(contribution);
var navigationTitle = BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId);
var navItem = new NavigationViewItem
{
Content = navigationTitle,
Tag = tag,
IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = FluentIcons.Common.Symbol.PuzzlePiece,
IconVariant = FluentIcons.Common.IconVariant.Regular
}
};
ToolTip.SetTip(navItem, $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}");
SettingsNavView.MenuItems.Insert(insertIndex++, navItem);
var pageHost = CreatePluginSettingsPageHost(contribution);
pageHost.IsVisible = false;
SettingsContentPagesHost.Children.Add(pageHost);
_pluginSettingsPageHosts[tag] = pageHost;
}
}
private static string BuildPluginSettingsTag(PluginSettingsPageContribution contribution)
{
return $"PluginPage:{contribution.Plugin.Manifest.Id}:{contribution.Registration.Id}";
}
private static string BuildPluginSettingsNavigationTitle(
PluginSettingsPageContribution contribution,
IReadOnlyDictionary<string, int> pageCountsByPluginId)
{
return pageCountsByPluginId.TryGetValue(contribution.Plugin.Manifest.Id, out var pageCount) && pageCount > 1
? $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"
: contribution.Plugin.Manifest.Name;
}
private Control CreatePluginSettingsPageHost(PluginSettingsPageContribution contribution)
{
Control content;
try
{
content = contribution.Registration.ContentFactory();
}
catch (Exception ex)
{
content = CreatePluginPageErrorContent(ex);
}
return new StackPanel
{
Spacing = 16,
Children =
{
new TextBlock
{
Text = contribution.Registration.Title,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush")
},
new TextBlock
{
Text = contribution.Plugin.Manifest.Name,
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush")
},
content
}
};
}
private static Control CreatePluginPageErrorContent(Exception exception)
{
return new Border
{
Background = new SolidColorBrush(Color.Parse("#332B0F16")),
BorderBrush = new SolidColorBrush(Color.Parse("#66F97316")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(16),
Child = new TextBlock
{
Text = exception.Message,
TextWrapping = TextWrapping.Wrap
}
};
}
private void UpdatePluginSettingsPageVisibility(string? selectedTag)
{
foreach (var pair in _pluginSettingsPageHosts)
{
pair.Value.IsVisible = string.Equals(pair.Key, selectedTag, StringComparison.OrdinalIgnoreCase);
}
}
private string? GetSelectedSettingsTabTag()
{
return (SettingsNavView?.SelectedItem as NavigationViewItem)?.Tag?.ToString();
}
private int ResolveSelectedSettingsTabIndex()
{
if (SettingsNavView?.SelectedItem is null || SettingsNavView.MenuItems is null)
{
return 0;
}
for (var i = 0; i < SettingsNavView.MenuItems.Count; i++)
{
if (ReferenceEquals(SettingsNavView.MenuItems[i], SettingsNavView.SelectedItem))
{
return i;
}
}
return 0;
}
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag))
{
var taggedItem = SettingsNavView.MenuItems
.OfType<NavigationViewItem>()
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), snapshot.SettingsTabTag, StringComparison.OrdinalIgnoreCase));
if (taggedItem is not null)
{
SettingsNavView.SelectedItem = taggedItem;
return;
}
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, SettingsNavView.MenuItems.Count - 1));
if (SettingsNavView.MenuItems[safeIndex] is NavigationViewItem navItem)
{
SettingsNavView.SelectedItem = navItem;
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private const int StatusBarRowIndex = 0;
private const string UpdateChannelStable = "Stable";
private const string UpdateChannelPreview = "Preview";
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 enum LauncherEntryKind
{
Folder,
Shortcut
}
private sealed record LauncherHiddenItemToken(LauncherEntryKind Kind, string Key);
private sealed record LauncherHiddenItemView(
LauncherEntryKind Kind,
string Key,
string DisplayName,
string Monogram,
Bitmap? IconBitmap);
private readonly Dictionary<string, Bitmap> _launcherIconCache = new(StringComparer.OrdinalIgnoreCase);
private ClockDisplayFormat _clockDisplayFormat = ClockDisplayFormat.HourMinuteSecond;
private bool _suppressStatusBarToggleEvents;
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 IDisposable? _persistSettingsDebounceTimer;
private bool IncludePrereleaseUpdates => string.Equals(
_updateChannel,
UpdateChannelPreview,
StringComparison.OrdinalIgnoreCase);
}

View File

@@ -0,0 +1,340 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
using Avalonia.Threading;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
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
{
AutoCheckUpdatesToggleSwitch.IsChecked = _autoCheckUpdates;
UpdateChannelChipListBox.SelectedIndex = IncludePrereleaseUpdates ? 1 : 0;
}
finally
{
_suppressUpdateOptionEvents = false;
}
UpdateUpdatePanelState();
}
private void ApplyUpdateLocalization()
{
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");
UpdateUpdatePanelState();
}
private async void OnCheckForUpdatesClick(object? sender, RoutedEventArgs e)
{
await CheckForUpdatesAsync(false);
}
private async void OnDownloadAndInstallUpdateClick(object? sender, RoutedEventArgs e)
{
if (_isCheckingUpdates || _isDownloadingUpdate)
{
return;
}
if (_latestReleaseInstallerAsset is null)
{
await CheckForUpdatesAsync(false);
}
if (_latestReleaseInstallerAsset is not null)
{
await DownloadAndInstallUpdateAsync(_latestReleaseInstallerAsset);
}
}
private void OnAutoCheckUpdatesToggled(object? sender, RoutedEventArgs e)
{
if (_suppressUpdateOptionEvents)
{
return;
}
_autoCheckUpdates = AutoCheckUpdatesToggleSwitch.IsChecked == true;
PersistSettings();
}
private void OnUpdateChannelSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressUpdateOptionEvents)
{
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
});
_updateStatusText = L("settings.update.status_installer_started", "Installer started. The app will close for update.");
UpdateUpdatePanelState();
Dispatcher.UIThread.Post(() =>
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.Shutdown();
}
else
{
Close();
}
}, DispatcherPriority.Background);
}
catch (Exception ex)
{
_updateStatusText = Lf("settings.update.status_launch_failed_format", "Failed to start installer: {0}", ex.Message);
UpdateUpdatePanelState();
}
}
private void UpdateUpdatePanelState()
{
UpdateCurrentVersionValueTextBlock.Text = GetAppVersionText();
UpdateLatestVersionValueTextBlock.Text = string.IsNullOrWhiteSpace(_latestReleaseVersionText) ? "-" : _latestReleaseVersionText;
UpdatePublishedAtValueTextBlock.Text = _latestReleasePublishedAt.HasValue && _latestReleasePublishedAt.Value != DateTimeOffset.MinValue
? _latestReleasePublishedAt.Value.LocalDateTime.ToString("yyyy-MM-dd HH:mm")
: "-";
UpdateStatusTextBlock.Text = string.IsNullOrWhiteSpace(_updateStatusText) ? L("settings.update.status_ready", "Ready to check for updates.") : _updateStatusText;
UpdateDownloadProgressTextBlock.Text = string.IsNullOrWhiteSpace(_updateDownloadProgressText) ? L("settings.update.download_progress_idle", "Download progress: -") : _updateDownloadProgressText;
UpdateDownloadProgressBar.IsVisible = _isDownloadingUpdate;
UpdateDownloadProgressBar.Value = Math.Clamp(_updateDownloadProgressPercent, 0d, 100d);
CheckForUpdatesButton.IsEnabled = !_isCheckingUpdates && !_isDownloadingUpdate;
DownloadAndInstallUpdateButton.IsEnabled = !_isCheckingUpdates && !_isDownloadingUpdate && _latestReleaseInstallerAsset is not null;
}
private static string NormalizeUpdateChannel(string? channel, bool includePrereleaseFallback)
{
if (string.Equals(channel, UpdateChannelPreview, StringComparison.OrdinalIgnoreCase))
{
return UpdateChannelPreview;
}
if (string.Equals(channel, UpdateChannelStable, StringComparison.OrdinalIgnoreCase))
{
return UpdateChannelStable;
}
return includePrereleaseFallback ? UpdateChannelPreview : UpdateChannelStable;
}
private string GetLocalizedUpdateChannelName(string channel)
{
return string.Equals(channel, UpdateChannelPreview, StringComparison.OrdinalIgnoreCase)
? L("settings.update.channel_preview", "Preview")
: L("settings.update.channel_stable", "Stable");
}
private static string BuildUpdateInstallerPath(string assetName)
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var updatesDirectory = Path.Combine(appData, "LanMountainDesktop", "Updates");
Directory.CreateDirectory(updatesDirectory);
var safeName = SanitizeFileName(assetName);
return Path.Combine(updatesDirectory, safeName);
}
private static string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
{
return $"LanMountainDesktop-Update-{DateTime.Now:yyyyMMddHHmmss}.exe";
}
var sanitized = fileName;
foreach (var c in Path.GetInvalidFileNameChars())
{
sanitized = sanitized.Replace(c, '_');
}
return sanitized;
}
}

View File

@@ -0,0 +1,754 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Platform.Storage;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LibVLCSharp.Shared;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void OnNightModeChecked(object? sender, RoutedEventArgs e)
{
if (_suppressThemeToggleEvents)
{
return;
}
ApplyNightModeState(true, refreshPalettes: true);
}
private void OnNightModeUnchecked(object? sender, RoutedEventArgs e)
{
if (_suppressThemeToggleEvents)
{
return;
}
ApplyNightModeState(false, refreshPalettes: true);
}
private async void OnPickWallpaperClick(object? sender, RoutedEventArgs e)
{
if (StorageProvider is null)
{
_wallpaperStatus = L("settings.wallpaper.storage_unavailable", "Storage provider is unavailable.");
UpdateWallpaperDisplay();
return;
}
var options = new FilePickerOpenOptions
{
Title = L("filepicker.title", "Select wallpaper"),
AllowMultiple = false,
FileTypeFilter =
[
new FilePickerFileType(L("filepicker.image_files", "Image files")) { Patterns = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif", "*.webp"] },
new FilePickerFileType(L("filepicker.video_files", "Video files")) { Patterns = ["*.mp4", "*.mkv", "*.webm", "*.avi", "*.mov", "*.m4v"] }
]
};
var files = await StorageProvider.OpenFilePickerAsync(options);
if (files.Count == 0)
{
return;
}
try
{
var importedPath = await ImportWallpaperAssetAsync(files[0]);
if (string.IsNullOrWhiteSpace(importedPath))
{
_wallpaperStatus = L("settings.wallpaper.import_failed", "Failed to import wallpaper file.");
UpdateWallpaperDisplay();
return;
}
_wallpaperPath = importedPath;
var mediaType = DetectWallpaperMediaType(importedPath);
switch (mediaType)
{
case WallpaperMediaType.Image:
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = new Bitmap(importedPath);
_wallpaperVideoPath = null;
_wallpaperMediaType = WallpaperMediaType.Image;
_wallpaperStatus = L("settings.wallpaper.image_applied", "Image wallpaper applied.");
break;
case WallpaperMediaType.Video:
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
_wallpaperVideoPath = importedPath;
_wallpaperMediaType = WallpaperMediaType.Video;
_wallpaperStatus = L("settings.wallpaper.video_applied", "Video wallpaper applied.");
break;
default:
_wallpaperStatus = L("settings.wallpaper.unsupported_file", "Selected file type is not supported.");
UpdateWallpaperDisplay();
return;
}
ApplyWallpaperBrush();
UpdateWallpaperDisplay();
RefreshColorPalettes();
EnsureSelectedThemeColor();
UpdateThemeColorSelectionState();
ThemeColorStatusTextBlock.Text = _wallpaperMediaType == WallpaperMediaType.Video
? L("settings.color.theme_updated_video", "Video wallpaper updated. Theme colors refreshed.")
: L("settings.color.theme_updated_wallpaper", "Wallpaper updated. Monet colors refreshed.");
PersistSettings();
}
catch (Exception ex)
{
_wallpaperStatus = Lf("settings.wallpaper.apply_failed_format", "Failed to apply wallpaper: {0}", ex.Message);
UpdateWallpaperDisplay();
}
}
private void OnClearWallpaperClick(object? sender, RoutedEventArgs e)
{
StopVideoWallpaper();
_wallpaperVideoPath = null;
_wallpaperMediaType = WallpaperMediaType.None;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
_wallpaperPath = null;
_wallpaperStatus = L("settings.wallpaper.cleared", "Background reset to solid color.");
ApplyWallpaperBrush();
UpdateWallpaperDisplay();
RefreshColorPalettes();
EnsureSelectedThemeColor();
UpdateThemeColorSelectionState();
ThemeColorStatusTextBlock.Text = L("settings.color.theme_cleared_wallpaper", "Wallpaper cleared. Monet colors refreshed.");
PersistSettings();
}
private void OnWallpaperPlacementSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
ApplyWallpaperBrush();
if (_wallpaperMediaType == WallpaperMediaType.Image && _wallpaperBitmap is not null)
{
_wallpaperStatus = Lf("settings.wallpaper.mode_format", "Wallpaper mode: {0}.", GetLocalizedPlacementDisplayName(GetSelectedWallpaperPlacement()));
}
else if (_wallpaperMediaType == WallpaperMediaType.Video)
{
_wallpaperStatus = L("settings.wallpaper.video_mode", "Video wallpaper mode uses automatic fill mode.");
}
UpdateWallpaperDisplay();
PersistSettings();
}
private void ApplyWallpaperBrush()
{
if (_wallpaperMediaType == WallpaperMediaType.Video && !string.IsNullOrWhiteSpace(_wallpaperVideoPath))
{
DesktopWallpaperLayer.Background = Brushes.Transparent;
WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground();
PlayVideoWallpaper(_wallpaperVideoPath);
return;
}
StopVideoWallpaper();
if (_wallpaperBitmap is null)
{
var fallbackBackground = GetThemeDefaultDesktopBackground();
DesktopWallpaperLayer.Background = fallbackBackground;
WallpaperPreviewViewport.Background = fallbackBackground;
return;
}
var placement = GetSelectedWallpaperPlacement();
DesktopWallpaperLayer.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, false);
WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, true);
}
private void UpdateWallpaperDisplay()
{
WallpaperPathTextBlock.Text = string.IsNullOrWhiteSpace(_wallpaperPath)
? L("settings.wallpaper.no_selection", "No wallpaper selected.")
: Path.GetFileName(_wallpaperPath);
WallpaperStatusTextBlock.Text = _wallpaperStatus;
WallpaperPlacementComboBox.IsEnabled = _wallpaperMediaType != WallpaperMediaType.Video;
if (_wallpaperMediaType == WallpaperMediaType.Video)
{
WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground();
return;
}
if (_wallpaperBitmap is null)
{
WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground();
return;
}
WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, GetSelectedWallpaperPlacement(), true);
}
private ImageBrush CreateWallpaperBrush(Bitmap bitmap, WallpaperPlacement placement, bool forPreview)
{
var brush = new ImageBrush
{
Source = bitmap,
Stretch = Stretch.UniformToFill,
AlignmentX = AlignmentX.Center,
AlignmentY = AlignmentY.Center,
TileMode = TileMode.None
};
if (forPreview && placement == WallpaperPlacement.Center)
{
const double nominalScreenWidth = 1920.0;
const double previewWidth = 480.0;
brush.Transform = new ScaleTransform(previewWidth / nominalScreenWidth, previewWidth / nominalScreenWidth);
}
switch (placement)
{
case WallpaperPlacement.Fit:
brush.Stretch = Stretch.Uniform;
break;
case WallpaperPlacement.Stretch:
brush.Stretch = Stretch.Fill;
break;
case WallpaperPlacement.Center:
brush.Stretch = Stretch.None;
break;
case WallpaperPlacement.Tile:
brush.Stretch = Stretch.None;
brush.TileMode = TileMode.Tile;
var tileSize = forPreview ? 96d : 220d;
brush.DestinationRect = new RelativeRect(0, 0, tileSize, tileSize, RelativeUnit.Absolute);
break;
}
return brush;
}
private WallpaperPlacement GetSelectedWallpaperPlacement()
{
return WallpaperPlacementComboBox.SelectedIndex switch
{
1 => WallpaperPlacement.Fit,
2 => WallpaperPlacement.Stretch,
3 => WallpaperPlacement.Center,
4 => WallpaperPlacement.Tile,
_ => WallpaperPlacement.Fill
};
}
private static string GetPlacementDisplayName(WallpaperPlacement placement)
{
return placement switch
{
WallpaperPlacement.Fill => "Fill",
WallpaperPlacement.Fit => "Fit",
WallpaperPlacement.Stretch => "Stretch",
WallpaperPlacement.Center => "Center",
WallpaperPlacement.Tile => "Tile",
_ => "Fill"
};
}
private IBrush GetThemeDefaultDesktopBackground()
{
if (Resources.TryGetResource("AdaptiveSurfaceBaseBrush", ActualThemeVariant, out var resource) &&
resource is IBrush themedBrush)
{
return themedBrush;
}
return _defaultDesktopBackground ??
(_isNightMode
? new SolidColorBrush(Color.Parse("#FF0B1220"))
: new SolidColorBrush(Color.Parse("#FFF3F7FB")));
}
private static int GetPlacementIndexFromSetting(string? placement)
{
if (string.IsNullOrWhiteSpace(placement))
{
return 0;
}
return placement.Trim().ToLowerInvariant() switch
{
"fit" => 1,
"stretch" => 2,
"center" => 3,
"tile" => 4,
_ => 0
};
}
private void TryRestoreWallpaper(string? savedWallpaperPath)
{
StopVideoWallpaper();
_wallpaperMediaType = WallpaperMediaType.None;
_wallpaperVideoPath = null;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
_wallpaperPath = null;
if (string.IsNullOrWhiteSpace(savedWallpaperPath))
{
_wallpaperStatus = L("settings.wallpaper.default_status", "Current background uses solid color.");
return;
}
if (!Path.IsPathRooted(savedWallpaperPath) || !File.Exists(savedWallpaperPath))
{
_wallpaperStatus = L("settings.wallpaper.saved_not_found", "Saved wallpaper file was not found. Using solid color background.");
return;
}
try
{
var mediaType = DetectWallpaperMediaType(savedWallpaperPath);
switch (mediaType)
{
case WallpaperMediaType.Image:
_wallpaperBitmap = new Bitmap(savedWallpaperPath);
_wallpaperPath = savedWallpaperPath;
_wallpaperMediaType = WallpaperMediaType.Image;
_wallpaperStatus = L("settings.wallpaper.restored", "Wallpaper restored from saved settings.");
break;
case WallpaperMediaType.Video:
_wallpaperVideoPath = savedWallpaperPath;
_wallpaperPath = savedWallpaperPath;
_wallpaperMediaType = WallpaperMediaType.Video;
_wallpaperStatus = L("settings.wallpaper.video_restored", "Video wallpaper restored from saved settings.");
break;
default:
_wallpaperStatus = L("settings.wallpaper.unsupported_file", "Saved wallpaper type is not supported. Using solid color background.");
break;
}
}
catch
{
_wallpaperStatus = L("settings.wallpaper.restore_failed", "Failed to restore saved wallpaper. Using solid color background.");
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
_wallpaperMediaType = WallpaperMediaType.None;
_wallpaperVideoPath = null;
_wallpaperPath = null;
}
}
private static bool TryParseColor(string? colorText, out Color color)
{
color = default;
if (string.IsNullOrWhiteSpace(colorText))
{
return false;
}
try
{
color = Color.Parse(colorText);
return true;
}
catch
{
return false;
}
}
private static WallpaperMediaType DetectWallpaperMediaType(string path)
{
var extension = Path.GetExtension(path);
if (string.IsNullOrWhiteSpace(extension))
{
return WallpaperMediaType.None;
}
if (SupportedImageExtensions.Contains(extension))
{
return WallpaperMediaType.Image;
}
if (SupportedVideoExtensions.Contains(extension))
{
return WallpaperMediaType.Video;
}
return WallpaperMediaType.None;
}
private static async Task<string?> ImportWallpaperAssetAsync(IStorageFile file)
{
try
{
var extension = Path.GetExtension(file.Name);
if (string.IsNullOrWhiteSpace(extension))
{
extension = ".bin";
}
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var wallpaperDirectory = Path.Combine(appData, "LanMountainDesktop", "Wallpapers");
Directory.CreateDirectory(wallpaperDirectory);
var destinationPath = Path.Combine(wallpaperDirectory, $"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{extension}");
await using var sourceStream = await file.OpenReadAsync();
await using var destinationStream = File.Create(destinationPath);
await sourceStream.CopyToAsync(destinationStream);
return destinationPath;
}
catch
{
return null;
}
}
private void EnsureVideoWallpaperPlayers()
{
Core.Initialize();
_libVlc ??= new LibVLC();
_previewVideoWallpaperPlayer ??= new MediaPlayer(_libVlc);
if (WallpaperPreviewVideoView is not null)
{
WallpaperPreviewVideoView.MediaPlayer = _previewVideoWallpaperPlayer;
}
}
private void PlayVideoWallpaper(string videoPath)
{
if (!File.Exists(videoPath))
{
_wallpaperStatus = L("settings.wallpaper.video_not_found", "Video wallpaper file not found.");
StopVideoWallpaper();
return;
}
try
{
EnsureVideoWallpaperPlayers();
if (_previewVideoWallpaperPlayer is null || _libVlc is null || WallpaperPreviewVideoView is null)
{
_wallpaperStatus = L("settings.wallpaper.video_player_unavailable", "Video player is unavailable.");
StopVideoWallpaper();
return;
}
_previewVideoWallpaperMedia?.Dispose();
_previewVideoWallpaperMedia = new Media(_libVlc, new Uri(videoPath));
_previewVideoWallpaperMedia.AddOption(":input-repeat=65535");
_previewVideoWallpaperPlayer.Play(_previewVideoWallpaperMedia);
WallpaperPreviewVideoView.IsVisible = true;
}
catch (Exception ex)
{
_wallpaperStatus = Lf("settings.wallpaper.video_play_failed_format", "Failed to play video wallpaper: {0}", ex.Message);
StopVideoWallpaper();
}
}
private void StopVideoWallpaper()
{
if (WallpaperPreviewVideoView is not null)
{
WallpaperPreviewVideoView.IsVisible = false;
}
_previewVideoWallpaperPlayer?.Stop();
_previewVideoWallpaperMedia?.Dispose();
_previewVideoWallpaperMedia = null;
}
private void OnRecommendedColorClick(object? sender, RoutedEventArgs e)
{
ApplyThemeColorFromButton(sender as Button, L("common.recommended", "Recommended"));
}
private void OnMonetColorClick(object? sender, RoutedEventArgs e)
{
ApplyThemeColorFromButton(sender as Button, L("common.monet", "Monet"));
}
private void OnRefreshMonetColorsClick(object? sender, RoutedEventArgs e)
{
RefreshColorPalettes();
EnsureSelectedThemeColor();
UpdateThemeColorSelectionState();
ThemeColorStatusTextBlock.Text = L("settings.color.monet_refreshed", "Monet colors refreshed.");
UpdateAdaptiveTextSystem();
PersistSettings();
}
private void ApplyNightModeState(bool enabled, bool refreshPalettes)
{
_isNightMode = enabled;
RequestedThemeVariant = enabled ? ThemeVariant.Dark : ThemeVariant.Light;
UpdateThemeModeIcon();
_suppressThemeToggleEvents = true;
NightModeToggleSwitch.IsChecked = enabled;
_suppressThemeToggleEvents = false;
if (refreshPalettes)
{
RefreshColorPalettes();
EnsureSelectedThemeColor();
}
UpdateThemeColorSelectionState();
ThemeColorStatusTextBlock.Text = Lf("settings.color.mode_status_format", "Theme mode: {0}.", enabled ? L("common.night", "Night") : L("common.day", "Day"));
UpdateAdaptiveTextSystem();
ApplyWallpaperBrush();
PersistSettings();
}
private void RefreshColorPalettes()
{
var palette = _monetColorService.BuildPalette(_wallpaperBitmap, _isNightMode);
_recommendedColors = palette.RecommendedColors;
_monetColors = palette.MonetColors;
ApplyColorPaletteToButtons(_recommendedColors, GetRecommendedColorTargets());
ApplyColorPaletteToButtons(_monetColors, GetMonetColorTargets());
}
private void ApplyColorPaletteToButtons(IReadOnlyList<Color> colors, IReadOnlyList<(Button Button, Border Swatch)> targets)
{
for (var i = 0; i < targets.Count; i++)
{
var color = i < colors.Count ? colors[i] : Color.Parse("#00000000");
var (button, swatch) = targets[i];
button.Tag = color.ToString();
button.IsEnabled = i < colors.Count;
swatch.Background = i < colors.Count ? new SolidColorBrush(color) : new SolidColorBrush(Color.Parse("#00000000"));
}
}
private IReadOnlyList<(Button Button, Border Swatch)> GetRecommendedColorTargets()
{
return
[
(RecommendedColorButton1, RecommendedColorSwatch1),
(RecommendedColorButton2, RecommendedColorSwatch2),
(RecommendedColorButton3, RecommendedColorSwatch3),
(RecommendedColorButton4, RecommendedColorSwatch4),
(RecommendedColorButton5, RecommendedColorSwatch5),
(RecommendedColorButton6, RecommendedColorSwatch6)
];
}
private IReadOnlyList<(Button Button, Border Swatch)> GetMonetColorTargets()
{
return
[
(MonetColorButton1, MonetColorSwatch1),
(MonetColorButton2, MonetColorSwatch2),
(MonetColorButton3, MonetColorSwatch3),
(MonetColorButton4, MonetColorSwatch4),
(MonetColorButton5, MonetColorSwatch5),
(MonetColorButton6, MonetColorSwatch6)
];
}
private void EnsureSelectedThemeColor()
{
if (ContainsColor(_recommendedColors, _selectedThemeColor) || ContainsColor(_monetColors, _selectedThemeColor))
{
return;
}
if (_recommendedColors.Count > 0)
{
_selectedThemeColor = _recommendedColors[0];
return;
}
if (_monetColors.Count > 0)
{
_selectedThemeColor = _monetColors[0];
}
}
private void ApplyThemeColorFromButton(Button? button, string sourceLabel)
{
if (!TryGetButtonColor(button, out var color))
{
return;
}
_selectedThemeColor = color;
UpdateThemeColorSelectionState();
ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_applied_format", "{0} color applied: {1}.", sourceLabel, _selectedThemeColor);
UpdateAdaptiveTextSystem();
PersistSettings();
}
private void UpdateThemeColorSelectionState()
{
UpdateColorSelectionVisuals(GetRecommendedColorTargets());
UpdateColorSelectionVisuals(GetMonetColorTargets());
}
private void UpdateColorSelectionVisuals(IReadOnlyList<(Button Button, Border Swatch)> targets)
{
foreach (var (button, swatch) in targets)
{
var isSelected = TryGetButtonColor(button, out var color) && AreSameColor(color, _selectedThemeColor);
button.Classes.Set("swatch-button", true);
button.Classes.Set("swatch-selected", isSelected);
swatch.BorderThickness = new Thickness(0);
swatch.Opacity = isSelected ? 1 : 0.9;
}
}
private static bool TryGetButtonColor(Button? button, out Color color)
{
color = default;
if (button?.Tag is not string colorText || string.IsNullOrWhiteSpace(colorText))
{
return false;
}
try
{
color = Color.Parse(colorText);
return true;
}
catch
{
return false;
}
}
private static bool ContainsColor(IReadOnlyList<Color> colors, Color target)
{
for (var i = 0; i < colors.Count; i++)
{
if (AreSameColor(colors[i], target))
{
return true;
}
}
return false;
}
private static bool AreSameColor(Color left, Color right)
{
return left.R == right.R && left.G == right.G && left.B == right.B;
}
private void UpdateAdaptiveTextSystem()
{
var context = new ThemeColorContext(_selectedThemeColor, !_isNightMode, !_isNightMode, _isNightMode);
ThemeColorSystemService.ApplyThemeResources(Resources, context);
GlassEffectService.ApplyGlassResources(Resources, context);
if (_fluentAvaloniaTheme is not null)
{
_fluentAvaloniaTheme.CustomAccentColor = _selectedThemeColor;
}
}
private double CalculateCurrentBackgroundLuminance()
{
if (_wallpaperMediaType == WallpaperMediaType.Video)
{
return CalculateRelativeLuminance(Color.Parse("#FF0B1220"));
}
if (_wallpaperBitmap is not null)
{
return CalculateBitmapAverageLuminance(_wallpaperBitmap);
}
return CalculateBrushLuminance(DesktopWallpaperLayer.Background ?? _defaultDesktopBackground);
}
private static double CalculateBrushLuminance(IBrush? brush)
{
if (brush is ISolidColorBrush solidBrush)
{
return CalculateRelativeLuminance(solidBrush.Color);
}
return CalculateRelativeLuminance(Color.Parse("#FF020617"));
}
private static double CalculateBitmapAverageLuminance(Bitmap bitmap)
{
try
{
var sampleWidth = Math.Clamp(bitmap.PixelSize.Width, 1, 48);
var sampleHeight = Math.Clamp(bitmap.PixelSize.Height, 1, 48);
using var scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize(sampleWidth, sampleHeight), BitmapInterpolationMode.MediumQuality);
using var writeable = new WriteableBitmap(scaledBitmap.PixelSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul);
using var framebuffer = writeable.Lock();
scaledBitmap.CopyPixels(framebuffer, AlphaFormat.Premul);
var rowBytes = framebuffer.RowBytes;
var byteCount = rowBytes * framebuffer.Size.Height;
if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero)
{
return CalculateRelativeLuminance(Color.Parse("#FF020617"));
}
var pixelBuffer = new byte[byteCount];
Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount);
double luminanceSum = 0;
var pixelCount = 0;
for (var y = 0; y < framebuffer.Size.Height; y++)
{
var rowOffset = y * rowBytes;
for (var x = 0; x < framebuffer.Size.Width; x++)
{
var index = rowOffset + (x * 4);
var alpha = pixelBuffer[index + 3] / 255d;
if (alpha <= 0.01)
{
continue;
}
var blue = Math.Clamp((pixelBuffer[index] / 255d) / alpha, 0, 1);
var green = Math.Clamp((pixelBuffer[index + 1] / 255d) / alpha, 0, 1);
var red = Math.Clamp((pixelBuffer[index + 2] / 255d) / alpha, 0, 1);
luminanceSum += CalculateRelativeLuminance(red, green, blue);
pixelCount++;
}
}
return pixelCount > 0 ? luminanceSum / pixelCount : CalculateRelativeLuminance(Color.Parse("#FF020617"));
}
catch
{
return CalculateRelativeLuminance(Color.Parse("#FF020617"));
}
}
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)
{
var linearRed = ToLinearRgb(red);
var linearGreen = ToLinearRgb(green);
var linearBlue = ToLinearRgb(blue);
return (0.2126 * linearRed) + (0.7152 * linearGreen) + (0.0722 * linearBlue);
}
private static double ToLinearRgb(double value)
{
return value <= 0.04045 ? value / 12.92 : Math.Pow((value + 0.055) / 1.055, 2.4);
}
}

View File

@@ -0,0 +1,916 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using FluentIcons.Common;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void InitializeWeatherSettings(AppSettingsSnapshot snapshot)
{
_suppressWeatherLocationEvents = true;
try
{
_weatherLocationMode = ParseWeatherLocationMode(snapshot.WeatherLocationMode);
_weatherLocationKey = snapshot.WeatherLocationKey?.Trim() ?? string.Empty;
_weatherLocationName = snapshot.WeatherLocationName?.Trim() ?? string.Empty;
_weatherLatitude = NormalizeLatitude(snapshot.WeatherLatitude);
_weatherLongitude = NormalizeLongitude(snapshot.WeatherLongitude);
_weatherAutoRefreshLocation = snapshot.WeatherAutoRefreshLocation;
_weatherExcludedAlertsRaw = snapshot.WeatherExcludedAlerts?.Trim() ?? string.Empty;
_weatherIconPackId = string.IsNullOrWhiteSpace(snapshot.WeatherIconPackId) ? "FluentRegular" : snapshot.WeatherIconPackId.Trim();
_weatherNoTlsRequests = snapshot.WeatherNoTlsRequests;
_weatherSearchKeyword = string.Empty;
var legacyQuery = snapshot.WeatherLocationQuery?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(_weatherLocationKey) && !string.IsNullOrWhiteSpace(legacyQuery))
{
_weatherLocationKey = legacyQuery;
}
if (string.IsNullOrWhiteSpace(_weatherLocationName) && !string.IsNullOrWhiteSpace(legacyQuery))
{
_weatherLocationName = legacyQuery;
}
SelectWeatherLocationModeInUi(_weatherLocationMode);
WeatherAutoRefreshToggleSwitch.IsChecked = _weatherAutoRefreshLocation;
WeatherNoTlsToggleSwitch.IsChecked = _weatherNoTlsRequests;
WeatherCitySearchTextBox.Text = string.Empty;
WeatherCityResultsComboBox.Items.Clear();
WeatherLocationKeyTextBox.Text = _weatherLocationKey;
WeatherLocationNameTextBox.Text = _weatherLocationName;
WeatherLatitudeNumberBox.Value = _weatherLatitude;
WeatherLongitudeNumberBox.Value = _weatherLongitude;
WeatherExcludedAlertsTextBox.Text = _weatherExcludedAlertsRaw;
SelectWeatherIconPackInUi(_weatherIconPackId);
WeatherSearchStatusTextBlock.Text = L("settings.weather.search_hint", "Search by city name and apply one location.");
WeatherCoordinateStatusTextBlock.Text = string.Empty;
WeatherPreviewResultTextBlock.Text = L("settings.weather.preview_hint", "Use test fetch to verify your weather configuration.");
UpdateWeatherPreviewSummary(weatherCode: null, temperatureText: "--", updatedAt: null);
}
finally
{
_suppressWeatherLocationEvents = false;
}
UpdateWeatherLocationModePanels();
UpdateWeatherLocationStatusText();
}
private void InitializeAutoStartWithWindowsSetting(AppSettingsSnapshot snapshot)
{
_autoStartWithWindows = OperatingSystem.IsWindows()
? _windowsStartupService.IsEnabled()
: snapshot.AutoStartWithWindows;
_suppressAutoStartToggleEvents = true;
try
{
AutoStartWithWindowsToggleSwitch.IsEnabled = OperatingSystem.IsWindows();
AutoStartWithWindowsToggleSwitch.IsChecked = _autoStartWithWindows;
}
finally
{
_suppressAutoStartToggleEvents = false;
}
}
private static WeatherLocationMode ParseWeatherLocationMode(string? value)
{
return string.Equals(value, "Coordinates", StringComparison.OrdinalIgnoreCase)
? WeatherLocationMode.Coordinates
: WeatherLocationMode.CitySearch;
}
private static string ToWeatherLocationModeTag(WeatherLocationMode mode)
{
return mode == WeatherLocationMode.Coordinates ? "Coordinates" : "CitySearch";
}
private static double NormalizeLatitude(double value)
{
if (double.IsNaN(value) || double.IsInfinity(value))
{
return 39.9042;
}
return Math.Clamp(value, -90, 90);
}
private static double NormalizeLongitude(double value)
{
if (double.IsNaN(value) || double.IsInfinity(value))
{
return 116.4074;
}
return Math.Clamp(value, -180, 180);
}
private string BuildLegacyWeatherLocationQuery()
{
if (!string.IsNullOrWhiteSpace(_weatherLocationName))
{
return _weatherLocationName;
}
if (!string.IsNullOrWhiteSpace(_weatherLocationKey))
{
return _weatherLocationKey;
}
return string.Create(CultureInfo.InvariantCulture, $"{_weatherLatitude:F4},{_weatherLongitude:F4}");
}
private void SelectWeatherLocationModeInUi(WeatherLocationMode mode)
{
var targetTag = ToWeatherLocationModeTag(mode);
foreach (var item in WeatherLocationModeComboBox.Items.OfType<ComboBoxItem>())
{
if (string.Equals(item.Tag?.ToString(), targetTag, StringComparison.OrdinalIgnoreCase))
{
WeatherLocationModeComboBox.SelectedItem = item;
break;
}
}
foreach (var item in WeatherLocationModeChipListBox.Items.OfType<ListBoxItem>())
{
if (string.Equals(item.Tag?.ToString(), targetTag, StringComparison.OrdinalIgnoreCase))
{
WeatherLocationModeChipListBox.SelectedItem = item;
return;
}
}
WeatherLocationModeComboBox.SelectedIndex = mode == WeatherLocationMode.Coordinates ? 1 : 0;
WeatherLocationModeChipListBox.SelectedIndex = mode == WeatherLocationMode.Coordinates ? 1 : 0;
}
private void SelectWeatherIconPackInUi(string iconPackId)
{
foreach (var item in WeatherIconPackComboBox.Items.OfType<ComboBoxItem>())
{
if (string.Equals(item.Tag?.ToString(), iconPackId, StringComparison.OrdinalIgnoreCase))
{
WeatherIconPackComboBox.SelectedItem = item;
return;
}
}
WeatherIconPackComboBox.SelectedIndex = 0;
_weatherIconPackId = "FluentRegular";
}
private void UpdateWeatherLocationModePanels()
{
WeatherCitySearchSettingsExpander.IsVisible = _weatherLocationMode == WeatherLocationMode.CitySearch;
WeatherCoordinateSettingsExpander.IsVisible = _weatherLocationMode == WeatherLocationMode.Coordinates;
}
private void OnWeatherLocationModeSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressWeatherLocationEvents || WeatherLocationModeComboBox.SelectedItem is not ComboBoxItem item)
{
return;
}
_weatherLocationMode = ParseWeatherLocationMode(item.Tag?.ToString());
_suppressWeatherLocationEvents = true;
try
{
SelectWeatherLocationModeInUi(_weatherLocationMode);
}
finally
{
_suppressWeatherLocationEvents = false;
}
UpdateWeatherLocationModePanels();
UpdateWeatherLocationStatusText();
PersistSettings();
}
private void OnWeatherLocationModeChipSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressWeatherLocationEvents || WeatherLocationModeChipListBox.SelectedItem is not ListBoxItem item)
{
return;
}
_weatherLocationMode = ParseWeatherLocationMode(item.Tag?.ToString());
_suppressWeatherLocationEvents = true;
try
{
SelectWeatherLocationModeInUi(_weatherLocationMode);
}
finally
{
_suppressWeatherLocationEvents = false;
}
UpdateWeatherLocationModePanels();
UpdateWeatherLocationStatusText();
PersistSettings();
}
private void OnWeatherAutoRefreshToggled(object? sender, RoutedEventArgs e)
{
if (_suppressWeatherLocationEvents)
{
return;
}
_weatherAutoRefreshLocation = WeatherAutoRefreshToggleSwitch.IsChecked == true;
PersistSettings();
}
private void OnWeatherExcludedAlertsLostFocus(object? sender, RoutedEventArgs e)
{
_weatherExcludedAlertsRaw = WeatherExcludedAlertsTextBox.Text?.Trim() ?? string.Empty;
PersistSettings();
}
private void OnWeatherIconPackSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressWeatherLocationEvents || WeatherIconPackComboBox.SelectedItem is not ComboBoxItem item)
{
return;
}
_weatherIconPackId = item.Tag?.ToString() switch
{
"FluentFilled" => "FluentFilled",
_ => "FluentRegular"
};
WeatherPreviewIconSymbol.IconVariant = string.Equals(_weatherIconPackId, "FluentFilled", StringComparison.OrdinalIgnoreCase)
? IconVariant.Filled
: IconVariant.Regular;
PersistSettings();
}
private void OnWeatherNoTlsToggled(object? sender, RoutedEventArgs e)
{
if (_suppressWeatherLocationEvents)
{
return;
}
_weatherNoTlsRequests = WeatherNoTlsToggleSwitch.IsChecked == true;
PersistSettings();
}
private void OnAutoStartWithWindowsToggled(object? sender, RoutedEventArgs e)
{
if (_suppressAutoStartToggleEvents)
{
return;
}
var requested = AutoStartWithWindowsToggleSwitch.IsChecked == true;
if (!OperatingSystem.IsWindows())
{
_autoStartWithWindows = false;
_suppressAutoStartToggleEvents = true;
try
{
AutoStartWithWindowsToggleSwitch.IsEnabled = false;
AutoStartWithWindowsToggleSwitch.IsChecked = false;
}
finally
{
_suppressAutoStartToggleEvents = false;
}
PersistSettings();
return;
}
var applied = _windowsStartupService.SetEnabled(requested);
_autoStartWithWindows = _windowsStartupService.IsEnabled();
if (!applied || _autoStartWithWindows != requested)
{
_suppressAutoStartToggleEvents = true;
try
{
AutoStartWithWindowsToggleSwitch.IsChecked = _autoStartWithWindows;
}
finally
{
_suppressAutoStartToggleEvents = false;
}
}
PersistSettings();
}
private async void OnSearchWeatherCityClick(object? sender, RoutedEventArgs e)
{
if (_isWeatherSearchInProgress)
{
return;
}
var keyword = WeatherCitySearchTextBox.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(keyword))
{
WeatherSearchStatusTextBlock.Text = L("settings.weather.search_required", "Please enter a city keyword first.");
return;
}
_weatherSearchKeyword = keyword;
_isWeatherSearchInProgress = true;
SetWeatherSearchBusy(true);
try
{
var result = await _weatherDataService.SearchLocationsAsync(keyword, ResolveWeatherApiLocale());
if (!result.Success || result.Data is null)
{
WeatherCityResultsComboBox.Items.Clear();
WeatherSearchStatusTextBlock.Text = Lf("settings.weather.search_failed_format", "Search failed: {0}", result.ErrorMessage ?? result.ErrorCode ?? "Unknown error");
return;
}
var locations = result.Data.Where(location => !string.IsNullOrWhiteSpace(location.LocationKey)).Take(80).ToList();
WeatherCityResultsComboBox.Items.Clear();
foreach (var location in locations)
{
WeatherCityResultsComboBox.Items.Add(new ComboBoxItem
{
Content = FormatWeatherLocationDisplayName(location),
Tag = location
});
}
WeatherSearchStatusTextBlock.Text = locations.Count == 0
? L("settings.weather.search_no_results", "No locations were found.")
: Lf("settings.weather.search_result_count_format", "Found {0} locations.", locations.Count);
if (locations.Count > 0)
{
WeatherCityResultsComboBox.SelectedIndex = 0;
}
}
catch (Exception ex)
{
WeatherSearchStatusTextBlock.Text = Lf("settings.weather.search_failed_format", "Search failed: {0}", ex.Message);
}
finally
{
_isWeatherSearchInProgress = false;
SetWeatherSearchBusy(false);
}
}
private static string FormatWeatherLocationDisplayName(WeatherLocation location)
{
var affiliation = string.IsNullOrWhiteSpace(location.Affiliation) ? string.Empty : $" ({location.Affiliation})";
return string.Create(CultureInfo.InvariantCulture, $"{location.Name}{affiliation} | {location.LocationKey}");
}
private static string BuildWeatherLocationName(WeatherLocation location)
{
if (string.IsNullOrWhiteSpace(location.Affiliation))
{
return location.Name;
}
return string.Create(CultureInfo.InvariantCulture, $"{location.Name} ({location.Affiliation})");
}
private void OnApplyWeatherCitySelectionClick(object? sender, RoutedEventArgs e)
{
if (WeatherCityResultsComboBox.SelectedItem is not ComboBoxItem item || item.Tag is not WeatherLocation location)
{
WeatherSearchStatusTextBlock.Text = L("settings.weather.search_select_required", "Please select one location from search results.");
return;
}
_weatherLocationMode = WeatherLocationMode.CitySearch;
_weatherLocationKey = location.LocationKey.Trim();
_weatherLocationName = BuildWeatherLocationName(location);
_weatherLatitude = NormalizeLatitude(location.Latitude);
_weatherLongitude = NormalizeLongitude(location.Longitude);
_suppressWeatherLocationEvents = true;
try
{
SelectWeatherLocationModeInUi(_weatherLocationMode);
WeatherLocationKeyTextBox.Text = _weatherLocationKey;
WeatherLocationNameTextBox.Text = _weatherLocationName;
WeatherLatitudeNumberBox.Value = _weatherLatitude;
WeatherLongitudeNumberBox.Value = _weatherLongitude;
}
finally
{
_suppressWeatherLocationEvents = false;
}
WeatherSearchStatusTextBlock.Text = Lf("settings.weather.search_applied_format", "Location applied: {0}", _weatherLocationName);
UpdateWeatherLocationModePanels();
UpdateWeatherLocationStatusText();
PersistSettings();
}
private void OnApplyWeatherCoordinatesClick(object? sender, RoutedEventArgs e)
{
var latitude = NormalizeLatitude(WeatherLatitudeNumberBox.Value);
var longitude = NormalizeLongitude(WeatherLongitudeNumberBox.Value);
var keyInput = WeatherLocationKeyTextBox.Text?.Trim() ?? string.Empty;
var nameInput = WeatherLocationNameTextBox.Text?.Trim() ?? string.Empty;
_weatherLocationMode = WeatherLocationMode.Coordinates;
_weatherLatitude = latitude;
_weatherLongitude = longitude;
_weatherLocationKey = string.IsNullOrWhiteSpace(keyInput) ? BuildCoordinateLocationKey(latitude, longitude) : keyInput;
_weatherLocationName = string.IsNullOrWhiteSpace(nameInput)
? Lf("settings.weather.coordinates_default_name_format", "Coordinate {0:F4}, {1:F4}", latitude, longitude)
: nameInput;
_suppressWeatherLocationEvents = true;
try
{
SelectWeatherLocationModeInUi(_weatherLocationMode);
if (string.IsNullOrWhiteSpace(keyInput))
{
WeatherLocationKeyTextBox.Text = _weatherLocationKey;
}
if (string.IsNullOrWhiteSpace(nameInput))
{
WeatherLocationNameTextBox.Text = _weatherLocationName;
}
}
finally
{
_suppressWeatherLocationEvents = false;
}
WeatherCoordinateStatusTextBlock.Text = Lf("settings.weather.coordinates_saved_format", "Coordinates saved: {0:F4}, {1:F4}", _weatherLatitude, _weatherLongitude);
UpdateWeatherLocationModePanels();
UpdateWeatherLocationStatusText();
PersistSettings();
}
private static string BuildCoordinateLocationKey(double latitude, double longitude)
{
return string.Create(CultureInfo.InvariantCulture, $"coord:{latitude:F4},{longitude:F4}");
}
private async void OnTestWeatherRequestClick(object? sender, RoutedEventArgs e)
{
if (_isWeatherPreviewInProgress)
{
return;
}
if (string.IsNullOrWhiteSpace(_weatherLocationKey))
{
if (_weatherLocationMode == WeatherLocationMode.Coordinates)
{
_weatherLocationKey = BuildCoordinateLocationKey(_weatherLatitude, _weatherLongitude);
}
else
{
WeatherPreviewResultTextBlock.Text = L("settings.weather.preview_missing_location", "Please apply one weather location before testing.");
UpdateWeatherPreviewSummary(null, "--", null);
return;
}
}
_isWeatherPreviewInProgress = true;
SetWeatherPreviewBusy(true);
try
{
var query = new WeatherQuery(_weatherLocationKey, _weatherLatitude, _weatherLongitude, 3, ResolveWeatherApiLocale(), false, true);
var result = await _weatherDataService.GetWeatherAsync(query);
if (!result.Success || result.Data is null)
{
WeatherPreviewResultTextBlock.Text = Lf("settings.weather.preview_failed_format", "Test fetch failed: {0}", result.ErrorMessage ?? result.ErrorCode ?? "Unknown error");
UpdateWeatherPreviewSummary(null, "--", DateTimeOffset.Now);
return;
}
var snapshot = result.Data;
var location = string.IsNullOrWhiteSpace(snapshot.LocationName)
? (!string.IsNullOrWhiteSpace(_weatherLocationName) ? _weatherLocationName : _weatherLocationKey)
: snapshot.LocationName;
var weather = snapshot.Current.WeatherText ?? L("settings.weather.preview_unknown", "Unknown");
var temperature = snapshot.Current.TemperatureC.HasValue
? string.Create(CultureInfo.InvariantCulture, $"{snapshot.Current.TemperatureC.Value:F1} C")
: "--";
var updatedAt = snapshot.ObservationTime ?? snapshot.FetchedAt;
WeatherPreviewResultTextBlock.Text = Lf("settings.weather.preview_success_format", "Test success: {0} | {1} | {2}", location, weather, temperature);
UpdateWeatherPreviewSummary(snapshot.Current.WeatherCode, temperature, updatedAt);
}
catch (Exception ex)
{
WeatherPreviewResultTextBlock.Text = Lf("settings.weather.preview_failed_format", "Test fetch failed: {0}", ex.Message);
UpdateWeatherPreviewSummary(null, "--", DateTimeOffset.Now);
}
finally
{
_isWeatherPreviewInProgress = false;
SetWeatherPreviewBusy(false);
}
}
private void UpdateWeatherPreviewSummary(int? weatherCode, string temperatureText, DateTimeOffset? updatedAt)
{
WeatherPreviewIconSymbol.Symbol = ResolveWeatherPreviewSymbol(weatherCode, _isNightMode);
WeatherPreviewIconSymbol.IconVariant = string.Equals(_weatherIconPackId, "FluentFilled", StringComparison.OrdinalIgnoreCase)
? IconVariant.Filled
: IconVariant.Regular;
WeatherPreviewTemperatureTextBlock.Text = string.IsNullOrWhiteSpace(temperatureText) ? "--" : temperatureText;
WeatherPreviewUpdatedTextBlock.Text = updatedAt.HasValue
? Lf("weather.widget.updated_format", "Updated {0:HH:mm}", updatedAt.Value.LocalDateTime)
: "-";
}
private static Symbol ResolveWeatherPreviewSymbol(int? weatherCode, bool isNight)
{
return weatherCode switch
{
0 => isNight ? Symbol.WeatherMoon : Symbol.WeatherSunny,
1 or 2 => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay,
3 or 7 => Symbol.WeatherRainShowersDay,
8 or 9 => Symbol.WeatherRain,
4 => Symbol.WeatherThunderstorm,
13 or 14 or 15 or 16 => Symbol.WeatherSnow,
18 or 32 => Symbol.WeatherFog,
_ => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay
};
}
private void SetWeatherSearchBusy(bool isBusy)
{
WeatherSearchButton.IsEnabled = !isBusy;
WeatherSearchProgressRing.IsVisible = isBusy;
}
private void SetWeatherPreviewBusy(bool isBusy)
{
WeatherPreviewButton.IsEnabled = !isBusy;
WeatherPreviewProgressRing.IsVisible = isBusy;
}
private string ResolveWeatherApiLocale()
{
return string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) ? "zh_cn" : "en_us";
}
private void UpdateWeatherLocationStatusText()
{
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.");
return;
}
var locationName = string.IsNullOrWhiteSpace(_weatherLocationName) ? _weatherLocationKey : _weatherLocationName;
WeatherLocationStatusTextBlock.Text = Lf("settings.weather.status_city_format", "Mode: {0} | {1} | Key: {2}", modeText, locationName, _weatherLocationKey);
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);
}
private void InitializeLauncherVisibilitySettings(LauncherSettingsSnapshot snapshot)
{
_hiddenLauncherFolderPaths.Clear();
if (snapshot.HiddenLauncherFolderPaths is not null)
{
foreach (var folderPath in snapshot.HiddenLauncherFolderPaths)
{
var key = NormalizeLauncherHiddenKey(folderPath);
if (!string.IsNullOrWhiteSpace(key))
{
_hiddenLauncherFolderPaths.Add(key);
}
}
}
_hiddenLauncherAppPaths.Clear();
if (snapshot.HiddenLauncherAppPaths is not null)
{
foreach (var appPath in snapshot.HiddenLauncherAppPaths)
{
var key = NormalizeLauncherHiddenKey(appPath);
if (!string.IsNullOrWhiteSpace(key))
{
_hiddenLauncherAppPaths.Add(key);
}
}
}
}
private async Task LoadLauncherEntriesAsync()
{
try
{
var loadResult = await Task.Run(() =>
{
var loadedRoot = OperatingSystem.IsLinux() ? _linuxDesktopEntryService.Load() : _windowsStartMenuService.Load();
var folderIconBytes = OperatingSystem.IsWindows() ? WindowsIconService.TryGetSystemFolderIconPngBytes() : null;
return (Root: loadedRoot, FolderIcon: folderIconBytes);
});
await Dispatcher.UIThread.InvokeAsync(() =>
{
_startMenuRoot = loadResult.Root;
_launcherFolderIconPngBytes = loadResult.FolderIcon;
_launcherFolderIconBitmap?.Dispose();
_launcherFolderIconBitmap = null;
RenderLauncherHiddenItemsList();
}, DispatcherPriority.Background);
}
catch
{
_startMenuRoot = new StartMenuFolderNode("All Apps", string.Empty);
_launcherFolderIconPngBytes = null;
_launcherFolderIconBitmap?.Dispose();
_launcherFolderIconBitmap = null;
RenderLauncherHiddenItemsList();
}
}
private static string NormalizeLauncherHiddenKey(string? key)
{
return string.IsNullOrWhiteSpace(key) ? string.Empty : key.Trim();
}
private void RenderLauncherHiddenItemsList()
{
LauncherHiddenItemsListPanel.Children.Clear();
var hiddenItems = BuildLauncherHiddenItems();
LauncherHiddenItemsEmptyTextBlock.IsVisible = hiddenItems.Count == 0;
if (hiddenItems.Count == 0)
{
return;
}
foreach (var hiddenItem in hiddenItems)
{
LauncherHiddenItemsListPanel.Children.Add(CreateLauncherHiddenItemRow(hiddenItem));
}
}
private IReadOnlyList<LauncherHiddenItemView> BuildLauncherHiddenItems()
{
var items = new List<LauncherHiddenItemView>();
var seenFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seenApps = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
CollectHiddenLauncherItems(_startMenuRoot, items, seenFolders, seenApps);
foreach (var key in _hiddenLauncherFolderPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
{
if (!seenFolders.Contains(key))
{
items.Add(new LauncherHiddenItemView(LauncherEntryKind.Folder, key, BuildLauncherHiddenFallbackDisplayName(key), "DIR", GetLauncherFolderIconBitmap()));
}
}
foreach (var key in _hiddenLauncherAppPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
{
if (!seenApps.Contains(key))
{
var fallbackName = BuildLauncherHiddenFallbackDisplayName(key);
items.Add(new LauncherHiddenItemView(LauncherEntryKind.Shortcut, key, fallbackName, BuildMonogram(fallbackName), null));
}
}
return items
.OrderBy(item => item.DisplayName, StringComparer.CurrentCultureIgnoreCase)
.ThenBy(item => item.Key, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private void CollectHiddenLauncherItems(StartMenuFolderNode folder, List<LauncherHiddenItemView> items, HashSet<string> seenFolders, HashSet<string> seenApps)
{
foreach (var subFolder in folder.Folders)
{
var folderKey = NormalizeLauncherHiddenKey(subFolder.RelativePath);
if (!string.IsNullOrWhiteSpace(folderKey) && _hiddenLauncherFolderPaths.Contains(folderKey) && seenFolders.Add(folderKey))
{
items.Add(new LauncherHiddenItemView(LauncherEntryKind.Folder, folderKey, subFolder.Name, "DIR", GetLauncherFolderIconBitmap()));
}
CollectHiddenLauncherItems(subFolder, items, seenFolders, seenApps);
}
foreach (var app in folder.Apps)
{
var appKey = NormalizeLauncherHiddenKey(app.RelativePath);
if (string.IsNullOrWhiteSpace(appKey) || !_hiddenLauncherAppPaths.Contains(appKey) || !seenApps.Add(appKey))
{
continue;
}
items.Add(new LauncherHiddenItemView(LauncherEntryKind.Shortcut, appKey, app.DisplayName, BuildMonogram(app.DisplayName), GetLauncherIconBitmap(app)));
}
}
private static string BuildLauncherHiddenFallbackDisplayName(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
return "Unknown";
}
var normalized = key.Replace('\\', '/');
var fileName = Path.GetFileNameWithoutExtension(normalized);
return string.IsNullOrWhiteSpace(fileName) ? key : fileName;
}
private Control CreateLauncherHiddenItemRow(LauncherHiddenItemView hiddenItem)
{
Control icon = hiddenItem.IconBitmap is not null
? new Image
{
Source = hiddenItem.IconBitmap,
Width = 24,
Height = 24,
Stretch = Stretch.Uniform
}
: new Border
{
Width = 24,
Height = 24,
CornerRadius = new CornerRadius(999),
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
Child = new TextBlock
{
Text = hiddenItem.Monogram,
FontSize = 10,
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
var typeText = hiddenItem.Kind == LauncherEntryKind.Folder
? L("settings.launcher.hidden_type_folder", "Folder")
: L("settings.launcher.hidden_type_shortcut", "Shortcut");
var infoPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 10,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
};
infoPanel.Children.Add(icon);
infoPanel.Children.Add(new StackPanel
{
Spacing = 2,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
Children =
{
new TextBlock { Text = hiddenItem.DisplayName, TextTrimming = TextTrimming.CharacterEllipsis, MaxLines = 1 },
new TextBlock { Text = typeText, FontSize = 11, Opacity = 0.7 }
}
});
var restoreButton = new Button
{
Content = L("settings.launcher.restore_button", "Show Again"),
MinWidth = 110,
Padding = new Thickness(12, 6),
Tag = new LauncherHiddenItemToken(hiddenItem.Kind, hiddenItem.Key)
};
restoreButton.Click += OnRestoreLauncherHiddenItemClick;
var row = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,Auto"),
ColumnSpacing = 10
};
row.Children.Add(infoPanel);
Grid.SetColumn(infoPanel, 0);
row.Children.Add(restoreButton);
Grid.SetColumn(restoreButton, 1);
return new Border
{
Classes = { "glass-panel" },
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(10, 8),
Child = row
};
}
private void OnRestoreLauncherHiddenItemClick(object? sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: LauncherHiddenItemToken token })
{
return;
}
var removed = token.Kind switch
{
LauncherEntryKind.Folder => _hiddenLauncherFolderPaths.Remove(token.Key),
LauncherEntryKind.Shortcut => _hiddenLauncherAppPaths.Remove(token.Key),
_ => false
};
if (!removed)
{
return;
}
RenderLauncherHiddenItemsList();
PersistSettings();
}
private Bitmap? GetLauncherIconBitmap(StartMenuAppEntry app)
{
if (app.IconPngBytes is null || app.IconPngBytes.Length == 0)
{
return null;
}
if (_launcherIconCache.TryGetValue(app.RelativePath, out var cached))
{
return cached;
}
try
{
using var stream = new MemoryStream(app.IconPngBytes, writable: false);
var bitmap = new Bitmap(stream);
_launcherIconCache[app.RelativePath] = bitmap;
return bitmap;
}
catch
{
return null;
}
}
private Bitmap? GetLauncherFolderIconBitmap()
{
if (_launcherFolderIconBitmap is not null)
{
return _launcherFolderIconBitmap;
}
if (_launcherFolderIconPngBytes is null || _launcherFolderIconPngBytes.Length == 0)
{
return null;
}
try
{
using var stream = new MemoryStream(_launcherFolderIconPngBytes, writable: false);
_launcherFolderIconBitmap = new Bitmap(stream);
return _launcherFolderIconBitmap;
}
catch
{
_launcherFolderIconBitmap = null;
return null;
}
}
private static string BuildMonogram(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return "?";
}
var letters = text.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(part => part[0]).Take(2).ToArray();
return letters.Length == 0 ? "?" : new string(letters).ToUpperInvariant();
}
}

View File

@@ -0,0 +1,179 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
xmlns:pages="using:LanMountainDesktop.Views.SettingsPages"
xmlns:comp="using:LanMountainDesktop.Views.Components"
x:Class="LanMountainDesktop.Views.SettingsWindow"
Title="Settings"
Icon="/Assets/avalonia-logo.ico"
Width="1360"
Height="900"
MinWidth="1120"
MinHeight="760"
ShowInTaskbar="True"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="SystemChrome"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<Grid x:Name="DesktopHost">
<Border x:Name="DesktopWallpaperLayer"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}" />
<Grid x:Name="SettingsPage"
Classes="settings-scope"
IsVisible="True"
Opacity="1"
Margin="20">
<Border x:Name="SettingsContentPanel"
Background="Transparent"
BorderThickness="0"
Margin="0"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0"
Classes="mica-strong"
CornerRadius="24,24,0,0"
Padding="20,16">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Width="40"
Height="40"
CornerRadius="20"
Background="{DynamicResource AdaptiveAccentBrush}">
<fi:FluentIcon Icon="Settings"
IconVariant="Regular"
Foreground="White"
FontSize="18"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Margin="14,0,0,0"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="WindowTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Text="Settings" />
<TextBlock x:Name="WindowSubtitleTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="LanMountainDesktop preferences" />
</StackPanel>
<Button Grid.Column="2"
Padding="10,8"
HorizontalAlignment="Right"
Click="OnCloseWindowClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="Dismiss" IconVariant="Regular" />
<TextBlock Text="Close" VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
<Border Grid.Row="1"
Classes="mica-strong"
CornerRadius="0,0,24,24"
Padding="18">
<ui:NavigationView x:Name="SettingsNavView"
PaneDisplayMode="Left"
IsSettingsVisible="False"
OpenPaneLength="240"
SelectionChanged="OnSettingsNavSelectionChanged">
<ui:NavigationView.MenuItems>
<ui:NavigationViewItem x:Name="SettingsNavWallpaperItem" Content="壁纸" Tag="Wallpaper">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Wallpaper" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavGridItem" Content="网格" Tag="Grid">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Grid" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavColorItem" Content="颜色" Tag="Color">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Color" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavStatusBarItem" Content="状态栏" Tag="StatusBar">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Status" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavWeatherItem" Content="天气" Tag="Weather">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="WeatherSunny" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavRegionItem" Content="地区" Tag="Region">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Globe" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavUpdateItem" Content="更新" Tag="Update">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="ArrowSync" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavAboutItem" Content="关于" Tag="About">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Info" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavLauncherItem" Content="应用启动台" Tag="Launcher">
<ui:NavigationViewItem.IconSource>
<ic:SymbolIconSource Symbol="Apps" IconVariant="Regular" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
<ui:NavigationViewItem x:Name="SettingsNavPluginsItem" Content="插件" Tag="Plugins">
<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" />
</Grid>
</ScrollViewer>
</ui:NavigationView>
</Border>
</Grid>
</Border>
</Grid>
<Grid IsVisible="False">
<Button x:Name="BackToWindowsButton" />
<Button x:Name="OpenComponentLibraryButton" />
<Button x:Name="OpenSettingsButton" />
<TextBlock x:Name="OpenSettingsButtonTextBlock" />
<Border x:Name="TaskbarFixedActionsHost" />
<Border x:Name="TaskbarSettingsActionHost" />
<StackPanel x:Name="TaskbarDynamicActionsHost" />
<Border x:Name="TopStatusBarHost">
<StackPanel x:Name="TopStatusComponentsPanel">
<comp:ClockWidget x:Name="ClockWidget" />
</StackPanel>
</Border>
<Border x:Name="BottomTaskbarContainer" />
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Styling;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components;
using LibVLCSharp.Shared;
using Line = Avalonia.Controls.Shapes.Line;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow : Window
{
private enum WallpaperPlacement
{
Fill,
Fit,
Stretch,
Center,
Tile
}
private enum WallpaperMediaType
{
None,
Image,
Video
}
private enum WeatherLocationMode
{
CitySearch,
Coordinates
}
private const int MinShortSideCells = 6;
private const int MaxShortSideCells = 96;
private const int MinEdgeInsetPercent = 0;
private const int MaxEdgeInsetPercent = 30;
private const int DefaultEdgeInsetPercent = 18;
private const double WallpaperPreviewMaxWidth = 520;
private const double LightBackgroundLuminanceThreshold = 0.57;
private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle";
private static readonly HashSet<string> SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp"
};
private static readonly HashSet<string> SupportedVideoExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v"
};
private static readonly TaskbarActionId[] DefaultPinnedTaskbarActions =
[
TaskbarActionId.MinimizeToWindows,
TaskbarActionId.OpenSettings
];
private readonly record struct GridMetrics(
int ColumnCount,
int RowCount,
double CellSize,
double GapPx,
double EdgeInsetPx,
double GridWidthPx,
double GridHeightPx)
{
public double Pitch => CellSize + GapPx;
}
private readonly MonetColorService _monetColorService = new();
private readonly AppSettingsService _appSettingsService = new();
private readonly LauncherSettingsService _launcherSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly TimeZoneService _timeZoneService = new();
private readonly WindowsStartupService _windowsStartupService = new();
private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
private readonly ComponentRegistry _componentRegistry;
private readonly WindowsStartMenuService _windowsStartMenuService = new();
private readonly LinuxDesktopEntryService _linuxDesktopEntryService = new();
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<TaskbarActionId> _pinnedTaskbarActions = [];
private readonly HashSet<string> _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly Stack<StartMenuFolderNode> _launcherFolderStack = [];
private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
private byte[]? _launcherFolderIconPngBytes;
private Bitmap? _launcherFolderIconBitmap;
private int _targetShortSideCells;
private bool _isSettingsOpen = true;
private bool _isNightMode;
private bool _enableDynamicTaskbarActions;
private bool _suppressThemeToggleEvents;
private bool _suppressLanguageSelectionEvents;
private bool _suppressTimeZoneSelectionEvents;
private bool _suppressWeatherLocationEvents;
private bool _suppressSettingsPersistence;
private bool _suppressGridSpacingEvents;
private bool _suppressGridInsetEvents;
private bool _suppressStatusBarSpacingEvents;
private bool _suppressAutoStartToggleEvents;
private bool _isUpdatingWallpaperPreviewLayout;
private IBrush? _defaultDesktopBackground;
private Bitmap? _wallpaperBitmap;
private WallpaperMediaType _wallpaperMediaType;
private string? _wallpaperVideoPath;
private MediaPlayer? _previewVideoWallpaperPlayer;
private Media? _previewVideoWallpaperMedia;
private LibVLC? _libVlc;
private string? _wallpaperPath;
private string _wallpaperStatus = "Current background uses solid color.";
private IReadOnlyList<Color> _recommendedColors = Array.Empty<Color>();
private IReadOnlyList<Color> _monetColors = Array.Empty<Color>();
private Color _selectedThemeColor = Color.Parse("#FF3B82F6");
private double _currentDesktopCellSize;
private double _currentDesktopCellGap;
private double _currentDesktopEdgeInset;
private string _gridSpacingPreset = "Relaxed";
private string _statusBarSpacingMode = "Relaxed";
private int _statusBarCustomSpacingPercent = 12;
private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent;
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
private string _languageCode = "zh-CN";
private WeatherLocationMode _weatherLocationMode = WeatherLocationMode.CitySearch;
private string _weatherLocationKey = string.Empty;
private string _weatherLocationName = string.Empty;
private double _weatherLatitude = 39.9042;
private double _weatherLongitude = 116.4074;
private bool _weatherAutoRefreshLocation;
private string _weatherExcludedAlertsRaw = string.Empty;
private string _weatherIconPackId = "FluentRegular";
private bool _weatherNoTlsRequests;
private bool _autoStartWithWindows;
private string _weatherSearchKeyword = string.Empty;
private bool _isWeatherSearchInProgress;
private bool _isWeatherPreviewInProgress;
public SettingsWindow()
{
_componentRegistry = DesktopComponentRegistryFactory.Create((Application.Current as App)?.PluginRuntimeService);
InitializeComponent();
InitializePluginSettingsNavigation();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
RequestedThemeVariant = Application.Current?.RequestedThemeVariant ?? ThemeVariant.Default;
HookEvents();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void HookEvents()
{
PickWallpaperButton.Click += OnPickWallpaperClick;
ClearWallpaperButton.Click += OnClearWallpaperClick;
WallpaperPlacementComboBox.SelectionChanged += OnWallpaperPlacementSelectionChanged;
GridSizeSlider.ValueChanged += OnGridSizeSliderChanged;
GridSpacingPresetComboBox.SelectionChanged += OnGridSpacingPresetSelectionChanged;
GridEdgeInsetSlider.ValueChanged += OnGridEdgeInsetSliderChanged;
ApplyGridButton.Click += OnApplyGridSizeClick;
NightModeToggleSwitch.Checked += OnNightModeChecked;
NightModeToggleSwitch.Unchecked += OnNightModeUnchecked;
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.Checked += OnStatusBarClockChecked;
StatusBarClockToggleSwitch.Unchecked += OnStatusBarClockUnchecked;
ClockFormatHMSSRadio.Checked += OnClockFormatChanged;
ClockFormatHMRadio.Checked += OnClockFormatChanged;
StatusBarSpacingModeComboBox.SelectionChanged += OnStatusBarSpacingModeChanged;
StatusBarSpacingSlider.ValueChanged += OnStatusBarSpacingSliderChanged;
WeatherPreviewButton.Click += OnTestWeatherRequestClick;
WeatherLocationModeComboBox.SelectionChanged += OnWeatherLocationModeSelectionChanged;
WeatherLocationModeChipListBox.SelectionChanged += OnWeatherLocationModeChipSelectionChanged;
WeatherAutoRefreshToggleSwitch.Checked += OnWeatherAutoRefreshToggled;
WeatherAutoRefreshToggleSwitch.Unchecked += OnWeatherAutoRefreshToggled;
WeatherSearchButton.Click += OnSearchWeatherCityClick;
WeatherApplyCityButton.Click += OnApplyWeatherCitySelectionClick;
WeatherApplyCoordinatesButton.Click += OnApplyWeatherCoordinatesClick;
WeatherExcludedAlertsTextBox.LostFocus += OnWeatherExcludedAlertsLostFocus;
WeatherIconPackComboBox.SelectionChanged += OnWeatherIconPackSelectionChanged;
WeatherNoTlsToggleSwitch.Checked += OnWeatherNoTlsToggled;
WeatherNoTlsToggleSwitch.Unchecked += OnWeatherNoTlsToggled;
LanguageComboBox.SelectionChanged += OnLanguageSelectionChanged;
TimeZoneComboBox.SelectionChanged += OnTimeZoneSelectionChanged;
AutoCheckUpdatesToggleSwitch.Checked += OnAutoCheckUpdatesToggled;
AutoCheckUpdatesToggleSwitch.Unchecked += OnAutoCheckUpdatesToggled;
UpdateChannelChipListBox.SelectionChanged += OnUpdateChannelSelectionChanged;
CheckForUpdatesButton.Click += OnCheckForUpdatesClick;
DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick;
AutoStartWithWindowsToggleSwitch.Checked += OnAutoStartWithWindowsToggled;
AutoStartWithWindowsToggleSwitch.Unchecked += OnAutoStartWithWindowsToggled;
Opened += OnWindowOpened;
}
private void OnWindowOpened(object? sender, EventArgs e)
{
Opened -= OnWindowOpened;
_suppressSettingsPersistence = true;
var snapshot = _appSettingsService.Load();
var launcherSnapshot = _launcherSettingsService.Load();
_targetShortSideCells = Math.Clamp(
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
MinShortSideCells,
MaxShortSideCells);
_gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
_statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
GridSizeNumberBox.Value = _targetShortSideCells;
GridSizeSlider.Value = _targetShortSideCells;
GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch
{
"Compact" => 0,
"Custom" => 2,
_ => 1
};
StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged;
ApplyTaskbarSettings(snapshot);
InitializeLocalization(snapshot.LanguageCode);
InitializeWeatherSettings(snapshot);
InitializeAutoStartWithWindowsSetting(snapshot);
InitializeUpdateSettings(snapshot);
InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeSettingsIcons();
ApplyLocalization();
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
TryRestoreWallpaper(snapshot.WallpaperPath);
RefreshColorPalettes();
if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
{
_selectedThemeColor = savedThemeColor;
}
_isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
ApplyNightModeState(_isNightMode, refreshPalettes: false);
EnsureSelectedThemeColor();
UpdateThemeColorSelectionState();
ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor);
WindowTitleTextBlock.Text = L("settings.title", "Settings");
WindowSubtitleTextBlock.Text = L("settings.footer", "LanMountainDesktop Settings");
_defaultDesktopBackground = DesktopWallpaperLayer.Background;
RestoreSettingsTabSelection(snapshot);
UpdateSettingsTabContent();
UpdateWallpaperDisplay();
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
InitializeTimeZoneSettings();
_ = LoadLauncherEntriesAsync();
_suppressSettingsPersistence = false;
}
private void OnCloseWindowClick(object? sender, RoutedEventArgs e)
{
Close();
}
}

Some files were not shown because too many files have changed in this diff Show More