setting_re2

设置架构革新中
This commit is contained in:
lincube
2026-03-13 00:33:00 +08:00
parent 40a3a00cfe
commit c4df243610
92 changed files with 2048 additions and 10520 deletions

View File

@@ -16,7 +16,6 @@ public sealed class SamplePlugin : PluginBase, IDisposable
var hostName = GetHostProperty(context, PluginHostPropertyKeys.HostApplicationName, "UnknownHost");
var hostVersion = GetHostProperty(context, PluginHostPropertyKeys.HostVersion, "UnknownVersion");
var sdkApiVersion = GetHostProperty(context, PluginHostPropertyKeys.PluginSdkApiVersion, "UnknownApiVersion");
var hostApplicationLifecycle = context.GetService<IHostApplicationLifecycle>();
var messageBus = context.GetService<IPluginMessageBus>()
?? throw new InvalidOperationException("Plugin message bus is not available.");
@@ -44,31 +43,26 @@ public sealed class SamplePlugin : PluginBase, IDisposable
File.AppendAllText(logPath, initMessage + Environment.NewLine);
_stateService.MarkBackendReady(localizer.Format(
"status.backend.detail.log_written",
"初始化日志已写入:{0}",
"Initialization log written: {0}",
logPath));
}
catch (Exception ex)
{
_stateService.MarkBackendFaulted(localizer.Format(
"status.backend.detail.log_write_failed",
"初始化日志写入失败:{0}",
"Initialization log failed: {0}",
ex.Message));
throw;
}
_clockService.Start();
context.RegisterSettingsPage(new PluginSettingsPageRegistration(
"status",
localizer.GetString("settings.page_title", "插件状态"),
() => new SamplePluginSettingsView(context)));
context.RegisterDesktopComponent(new PluginDesktopComponentRegistration(
"LanMountainDesktop.SamplePlugin.StatusClock",
localizer.GetString("widget.display_name", "示例插件状态时钟"),
localizer.GetString("widget.display_name", "Sample Plugin Status Clock"),
widgetContext => new SamplePluginStatusClockWidget(widgetContext),
iconKey: "PuzzlePiece",
category: localizer.GetString("widget.category", "插件"),
category: localizer.GetString("widget.category", "Plugins"),
minWidthCells: 4,
minHeightCells: 4,
allowDesktopPlacement: true,
@@ -78,10 +72,10 @@ public sealed class SamplePlugin : PluginBase, IDisposable
context.RegisterDesktopComponent(new PluginDesktopComponentRegistration(
"LanMountainDesktop.SamplePlugin.CloseDesktop",
localizer.GetString("widget.close_desktop.display_name", "关闭桌面"),
localizer.GetString("widget.close_desktop.display_name", "Close Desktop"),
widgetContext => new SamplePluginCloseDesktopWidget(widgetContext),
iconKey: "DismissCircle",
category: localizer.GetString("widget.category", "鎻掍欢"),
category: localizer.GetString("widget.category", "Plugins"),
minWidthCells: 2,
minHeightCells: 1,
allowDesktopPlacement: true,

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public interface IComponentSettingsAccessor
{
string ComponentId { get; }
string? PlacementId { get; }
T LoadSnapshot<T>() where T : new();
void SaveSnapshot<T>(T snapshot, IReadOnlyCollection<string>? changedKeys = null);
T LoadSection<T>(string sectionId) where T : new();
void SaveSection<T>(string sectionId, T section, IReadOnlyCollection<string>? changedKeys = null);
void DeleteSection(string sectionId);
T? GetValue<T>(string key);
void SetValue<T>(string key, T value, IReadOnlyCollection<string>? changedKeys = null);
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public interface ISettingsCatalog
{
IReadOnlyList<SettingsSectionDefinition> GetSections();
IReadOnlyList<SettingsSectionDefinition> GetSections(SettingsScope scope);
}

View File

@@ -0,0 +1,56 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public interface ISettingsService
{
event EventHandler<SettingsChangedEvent>? Changed;
T LoadSnapshot<T>(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new();
void SaveSnapshot<T>(
SettingsScope scope,
T snapshot,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null);
T LoadSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null) where T : new();
void SaveSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
T section,
string? placementId = null,
IReadOnlyCollection<string>? changedKeys = null);
void DeleteSection(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null);
T? GetValue<T>(
SettingsScope scope,
string key,
string? subjectId = null,
string? placementId = null,
string? sectionId = null);
void SetValue<T>(
SettingsScope scope,
string key,
T value,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null);
IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId);
}

View File

@@ -5,20 +5,26 @@ namespace LanMountainDesktop.PluginSdk;
public static class PluginServiceCollectionExtensions
{
public static IServiceCollection AddPluginSettingsPage<TControl>(
public static IServiceCollection AddPluginSettingsSection(
this IServiceCollection services,
string id,
string title,
string titleLocalizationKey,
Action<PluginSettingsSectionBuilder> configure,
string? descriptionLocalizationKey = null,
string iconKey = "PuzzlePiece",
int sortOrder = 0)
where TControl : Control
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
services.AddSingleton(new PluginSettingsPageRegistration(
var builder = new PluginSettingsSectionBuilder(
id,
title,
provider => ActivatorUtilities.CreateInstance<TControl>(provider),
sortOrder));
titleLocalizationKey,
descriptionLocalizationKey,
iconKey,
sortOrder);
configure(builder);
services.AddSingleton(builder.Build());
return services;
}

View File

@@ -1,39 +0,0 @@
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginSettingsPageRegistration
{
public PluginSettingsPageRegistration(
string id,
string title,
Func<IServiceProvider, 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 PluginSettingsPageRegistration(
string id,
string title,
Func<Control> contentFactory,
int sortOrder = 0)
: this(id, title, _ => contentFactory(), sortOrder)
{
}
public string Id { get; }
public string Title { get; }
public int SortOrder { get; }
public Func<IServiceProvider, Control> ContentFactory { get; }
}

View File

@@ -0,0 +1,147 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginSettingsSectionBuilder
{
private readonly List<SettingsOptionDefinition> _options = [];
internal PluginSettingsSectionBuilder(
string id,
string titleLocalizationKey,
string? descriptionLocalizationKey,
string iconKey,
int sortOrder)
{
Id = id;
TitleLocalizationKey = titleLocalizationKey;
DescriptionLocalizationKey = descriptionLocalizationKey;
IconKey = iconKey;
SortOrder = sortOrder;
}
public string Id { get; }
public string TitleLocalizationKey { get; }
public string? DescriptionLocalizationKey { get; }
public string IconKey { get; }
public int SortOrder { get; }
public IReadOnlyList<SettingsOptionDefinition> Options => _options;
public PluginSettingsSectionBuilder AddOption(SettingsOptionDefinition option)
{
ArgumentNullException.ThrowIfNull(option);
_options.Add(option);
return this;
}
public PluginSettingsSectionBuilder AddToggle(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
bool defaultValue = false)
{
return AddOption(new SettingsOptionDefinition(
key,
SettingsOptionType.Toggle,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue));
}
public PluginSettingsSectionBuilder AddText(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
string defaultValue = "",
string? validationPattern = null)
{
return AddOption(new SettingsOptionDefinition(
key,
SettingsOptionType.Text,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue,
validationPattern: validationPattern));
}
public PluginSettingsSectionBuilder AddNumber(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
double defaultValue = 0,
double? minimum = null,
double? maximum = null)
{
return AddOption(new SettingsOptionDefinition(
key,
SettingsOptionType.Number,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue,
minimum: minimum,
maximum: maximum));
}
public PluginSettingsSectionBuilder AddSelect(
string key,
string titleLocalizationKey,
IEnumerable<SettingsOptionChoice> choices,
string? descriptionLocalizationKey = null,
string? defaultValue = null)
{
ArgumentNullException.ThrowIfNull(choices);
var normalizedChoices = choices.ToArray();
return AddOption(new SettingsOptionDefinition(
key,
SettingsOptionType.Select,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue,
normalizedChoices));
}
public PluginSettingsSectionBuilder AddPath(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
string defaultValue = "")
{
return AddOption(new SettingsOptionDefinition(
key,
SettingsOptionType.Path,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue));
}
public PluginSettingsSectionBuilder AddList(
string key,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
IReadOnlyList<string>? defaultValue = null)
{
return AddOption(new SettingsOptionDefinition(
key,
SettingsOptionType.List,
titleLocalizationKey,
descriptionLocalizationKey,
defaultValue ?? Array.Empty<string>()));
}
internal PluginSettingsSectionRegistration Build()
{
return new PluginSettingsSectionRegistration(
Id,
TitleLocalizationKey,
_options.ToArray(),
DescriptionLocalizationKey,
IconKey,
SortOrder);
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginSettingsSectionRegistration
{
public PluginSettingsSectionRegistration(
string id,
string titleLocalizationKey,
IReadOnlyList<SettingsOptionDefinition> options,
string? descriptionLocalizationKey = null,
string iconKey = "PuzzlePiece",
int sortOrder = 0)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey);
ArgumentException.ThrowIfNullOrWhiteSpace(iconKey);
Id = id.Trim();
TitleLocalizationKey = titleLocalizationKey.Trim();
DescriptionLocalizationKey = string.IsNullOrWhiteSpace(descriptionLocalizationKey)
? null
: descriptionLocalizationKey.Trim();
IconKey = iconKey.Trim();
SortOrder = sortOrder;
Options = options ?? [];
}
public string Id { get; }
public string TitleLocalizationKey { get; }
public string? DescriptionLocalizationKey { get; }
public string IconKey { get; }
public int SortOrder { get; }
public IReadOnlyList<SettingsOptionDefinition> Options { get; }
}

View File

@@ -0,0 +1,14 @@
namespace LanMountainDesktop.PluginSdk;
public static class SettingsCategories
{
public const string General = "General";
public const string Appearance = "Appearance";
public const string Components = "Components";
public const string Plugins = "Plugins";
public const string PluginMarket = "PluginMarket";
public const string Update = "Update";
public const string About = "About";
public const string Advanced = "Advanced";
public const string External = "External";
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public sealed class SettingsChangedEvent
{
public SettingsChangedEvent(
SettingsScope scope,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
Scope = scope;
SubjectId = string.IsNullOrWhiteSpace(subjectId) ? null : subjectId.Trim();
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
SectionId = string.IsNullOrWhiteSpace(sectionId) ? null : sectionId.Trim();
ChangedKeys = changedKeys is { Count: > 0 }
? changedKeys.ToArray()
: [];
}
public SettingsScope Scope { get; }
public string? SubjectId { get; }
public string? PlacementId { get; }
public string? SectionId { get; }
public IReadOnlyCollection<string> ChangedKeys { get; }
}

View File

@@ -0,0 +1,17 @@
namespace LanMountainDesktop.PluginSdk;
public sealed class SettingsOptionChoice
{
public SettingsOptionChoice(string value, string titleLocalizationKey)
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey);
Value = value.Trim();
TitleLocalizationKey = titleLocalizationKey.Trim();
}
public string Value { get; }
public string TitleLocalizationKey { get; }
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public sealed class SettingsOptionDefinition
{
public SettingsOptionDefinition(
string key,
SettingsOptionType optionType,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
object? defaultValue = null,
IReadOnlyList<SettingsOptionChoice>? choices = null,
double? minimum = null,
double? maximum = null,
string? validationPattern = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);
ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey);
Key = key.Trim();
OptionType = optionType;
TitleLocalizationKey = titleLocalizationKey.Trim();
DescriptionLocalizationKey = string.IsNullOrWhiteSpace(descriptionLocalizationKey)
? null
: descriptionLocalizationKey.Trim();
DefaultValue = defaultValue;
Choices = choices ?? [];
Minimum = minimum;
Maximum = maximum;
ValidationPattern = string.IsNullOrWhiteSpace(validationPattern)
? null
: validationPattern.Trim();
}
public string Key { get; }
public SettingsOptionType OptionType { get; }
public string TitleLocalizationKey { get; }
public string? DescriptionLocalizationKey { get; }
public object? DefaultValue { get; }
public IReadOnlyList<SettingsOptionChoice> Choices { get; }
public double? Minimum { get; }
public double? Maximum { get; }
public string? ValidationPattern { get; }
}

View File

@@ -0,0 +1,11 @@
namespace LanMountainDesktop.PluginSdk;
public enum SettingsOptionType
{
Toggle = 0,
Select = 1,
Text = 2,
Number = 3,
Path = 4,
List = 5
}

View File

@@ -0,0 +1,9 @@
namespace LanMountainDesktop.PluginSdk;
public enum SettingsScope
{
App = 0,
Launcher = 1,
Plugin = 2,
ComponentInstance = 3
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace LanMountainDesktop.PluginSdk;
public sealed class SettingsSectionDefinition
{
public SettingsSectionDefinition(
string id,
string category,
SettingsScope scope,
string titleLocalizationKey,
string? descriptionLocalizationKey = null,
string iconKey = "Settings",
int sortOrder = 0,
string? subjectId = null,
IReadOnlyList<SettingsOptionDefinition>? options = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ArgumentException.ThrowIfNullOrWhiteSpace(category);
ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey);
ArgumentException.ThrowIfNullOrWhiteSpace(iconKey);
Id = id.Trim();
Category = category.Trim();
Scope = scope;
TitleLocalizationKey = titleLocalizationKey.Trim();
DescriptionLocalizationKey = string.IsNullOrWhiteSpace(descriptionLocalizationKey)
? null
: descriptionLocalizationKey.Trim();
IconKey = iconKey.Trim();
SortOrder = sortOrder;
SubjectId = string.IsNullOrWhiteSpace(subjectId) ? null : subjectId.Trim();
Options = options ?? [];
}
public string Id { get; }
public string Category { get; }
public SettingsScope Scope { get; }
public string TitleLocalizationKey { get; }
public string? DescriptionLocalizationKey { get; }
public string IconKey { get; }
public int SortOrder { get; }
public string? SubjectId { get; }
public IReadOnlyList<SettingsOptionDefinition> Options { get; }
}

View File

@@ -40,7 +40,6 @@ public partial class App : Application
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
private ShutdownIntent _shutdownIntent;
private readonly IndependentSettingsModuleService _independentSettingsModuleService = new();
private TrayIcons? _trayIcons;
private PluginRuntimeService? _pluginRuntimeService;
private MainWindow? _mainWindow;
@@ -55,7 +54,9 @@ public partial class App : Application
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
{
_independentSettingsModuleService.ShowOrActivate(source, pageTag);
AppLogger.Info(
"SettingsFacade",
$"Settings UI entry is disabled by hard-cut migration. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
}
public override void Initialize()
@@ -105,11 +106,6 @@ public partial class App : Application
RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayMenu");
}
private void OnTraySettingsClick(object? sender, EventArgs e)
{
OpenIndependentSettingsModule("TrayMenu");
}
private void OnTrayRestartClick(object? sender, EventArgs e)
{
_ = _hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
@@ -206,12 +202,6 @@ public partial class App : Application
menu.Items.Add(new NativeMenuItemSeparator());
var settingsItem = new NativeMenuItem(L("tray.menu.settings", "Settings"));
settingsItem.Click += OnTraySettingsClick;
menu.Items.Add(settingsItem);
menu.Items.Add(new NativeMenuItemSeparator());
var restartItem = new NativeMenuItem(L("tray.menu.restart", "Restart App"));
restartItem.Click += OnTrayRestartClick;
menu.Items.Add(restartItem);
@@ -361,8 +351,6 @@ public partial class App : Application
_exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
_independentSettingsModuleService.CloseIfOpen();
try
{
_pluginRuntimeService?.Dispose();

View File

@@ -60,8 +60,7 @@ public sealed class AppSettingsSnapshot
public List<string> PinnedTaskbarActions { get; set; } =
[
TaskbarActionId.MinimizeToWindows.ToString(),
TaskbarActionId.OpenSettings.ToString()
TaskbarActionId.MinimizeToWindows.ToString()
];
public bool EnableDynamicTaskbarActions { get; set; } = true;

View File

@@ -1,12 +1,10 @@
namespace LanMountainDesktop.Models;
namespace LanMountainDesktop.Models;
public enum TaskbarActionId
{
MinimizeToWindows,
OpenSettings,
AddDesktopPage,
DeleteDesktopPage,
DeleteComponent,
EditComponent,
HideLauncherEntry
}

View File

@@ -2,11 +2,5 @@
public enum TaskbarContext
{
Desktop,
SettingsWallpaper,
SettingsGrid,
SettingsColor,
SettingsStatusBar,
SettingsWeather,
SettingsRegion
Desktop
}

View File

@@ -0,0 +1,116 @@
using System;
namespace LanMountainDesktop.Services;
public readonly record struct DesktopGridMetrics(
int ColumnCount,
int RowCount,
double CellSize,
double GapPx,
double EdgeInsetPx,
double GridWidthPx,
double GridHeightPx)
{
public double Pitch => CellSize + GapPx;
}
public sealed class DesktopGridLayoutService
{
public const string RelaxedSpacingPreset = "Relaxed";
public const string CompactSpacingPreset = "Compact";
public string NormalizeSpacingPreset(string? value)
{
return string.Equals(value, CompactSpacingPreset, StringComparison.OrdinalIgnoreCase)
? CompactSpacingPreset
: RelaxedSpacingPreset;
}
public double ResolveGapRatio(string? preset)
{
return string.Equals(preset, CompactSpacingPreset, StringComparison.OrdinalIgnoreCase) ? 0.06 : 0.12;
}
public 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;
var insetRatio = Math.Clamp(insetPercent, 0, 30) / 100d;
return Math.Clamp(baseCell * insetRatio, 0, 80);
}
public DesktopGridMetrics 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 DesktopGridMetrics(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 DesktopGridMetrics(
columnCountPortrait,
rowCountPortrait,
cellSizePortrait,
gapPxPortrait,
inset,
gridWidthPortrait,
gridHeightPortrait);
}
}

View File

@@ -1,88 +0,0 @@
using System;
using Avalonia.Threading;
using LanMountainDesktop.Views;
namespace LanMountainDesktop.Services;
internal sealed class IndependentSettingsModuleService
{
private SettingsWindow? _window;
public void ShowOrActivate(string source, string? pageTag = null)
{
AppLogger.Info("IndependentSettingsModule", $"OpenRequested; Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
void ShowCore()
{
try
{
if (_window is not { } window)
{
AppLogger.Info("IndependentSettingsModule", $"WindowConstructionStarted; Source='{source}'.");
window = new SettingsWindow();
AppLogger.Info("IndependentSettingsModule", $"WindowConstructionCompleted; Source='{source}'.");
window.Closed += (_, _) =>
{
if (ReferenceEquals(_window, window))
{
_window = null;
}
AppLogger.Info("IndependentSettingsModule", "WindowClosed.");
};
_window = window;
}
window.Open(pageTag);
AppLogger.Info(
"IndependentSettingsModule",
$"WindowActivated; Source='{source}'; ReusedExisting={ReferenceEquals(_window, window)}; WasVisible={window.IsVisible}; PageTag='{pageTag ?? "<default>"}'.");
}
catch (Exception ex)
{
AppLogger.Warn("IndependentSettingsModule", $"Failed to open independent settings module window. Source='{source}'.", ex);
}
}
if (Dispatcher.UIThread.CheckAccess())
{
ShowCore();
return;
}
Dispatcher.UIThread.Post(ShowCore, DispatcherPriority.Normal);
}
public void CloseIfOpen()
{
void CloseCore()
{
if (_window is null)
{
return;
}
try
{
_window.PrepareForForceClose();
_window.Close();
}
catch (Exception ex)
{
AppLogger.Warn("IndependentSettingsModule", "Failed to close independent settings module window during shutdown.", ex);
}
finally
{
_window = null;
}
}
if (Dispatcher.UIThread.CheckAccess())
{
CloseCore();
return;
}
Dispatcher.UIThread.Post(CloseCore, DispatcherPriority.Send);
}
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services.Settings;
internal sealed class SettingsCatalogService : ISettingsCatalog
{
private readonly List<SettingsSectionDefinition> _sections = [];
private readonly object _gate = new();
public SettingsCatalogService()
{
// Built-in host sections for the next settings UI.
_sections.AddRange(
[
new SettingsSectionDefinition("general", SettingsCategories.General, SettingsScope.App, "settings.general.title", iconKey: "Settings", sortOrder: 0),
new SettingsSectionDefinition("appearance", SettingsCategories.Appearance, SettingsScope.App, "settings.appearance.title", iconKey: "DesignIdeas", sortOrder: 10),
new SettingsSectionDefinition("components", SettingsCategories.Components, SettingsScope.ComponentInstance, "settings.components.title", iconKey: "GridDots", sortOrder: 20),
new SettingsSectionDefinition("plugins", SettingsCategories.Plugins, SettingsScope.Plugin, "settings.plugins.title", iconKey: "PuzzlePiece", sortOrder: 30),
new SettingsSectionDefinition("plugin-market", SettingsCategories.PluginMarket, SettingsScope.Plugin, "settings.plugin_market.title", iconKey: "Shop", sortOrder: 40),
new SettingsSectionDefinition("update", SettingsCategories.Update, SettingsScope.App, "settings.update.title", iconKey: "ArrowSync", sortOrder: 50),
new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 60),
new SettingsSectionDefinition("advanced", SettingsCategories.Advanced, SettingsScope.App, "settings.advanced.title", iconKey: "DeveloperBoard", sortOrder: 70)
]);
}
public IReadOnlyList<SettingsSectionDefinition> GetSections()
{
lock (_gate)
{
return _sections
.OrderBy(section => section.SortOrder)
.ThenBy(section => section.Id, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
}
public IReadOnlyList<SettingsSectionDefinition> GetSections(SettingsScope scope)
{
lock (_gate)
{
return _sections
.Where(section => section.Scope == scope)
.OrderBy(section => section.SortOrder)
.ThenBy(section => section.Id, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
}
public void RegisterPluginSections(string pluginId, IReadOnlyList<PluginSettingsSectionRegistration> sections)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pluginId);
var normalizedPluginId = pluginId.Trim();
lock (_gate)
{
_sections.RemoveAll(section =>
section.Scope == SettingsScope.Plugin &&
string.Equals(section.SubjectId, normalizedPluginId, StringComparison.OrdinalIgnoreCase));
foreach (var registration in sections)
{
var definition = new SettingsSectionDefinition(
id: $"{normalizedPluginId}:{registration.Id}",
category: SettingsCategories.External,
scope: SettingsScope.Plugin,
titleLocalizationKey: registration.TitleLocalizationKey,
descriptionLocalizationKey: registration.DescriptionLocalizationKey,
iconKey: registration.IconKey,
sortOrder: registration.SortOrder,
subjectId: normalizedPluginId,
options: registration.Options);
_sections.Add(definition);
}
}
}
public void RemovePluginSections(string pluginId)
{
if (string.IsNullOrWhiteSpace(pluginId))
{
return;
}
lock (_gate)
{
_sections.RemoveAll(section =>
section.Scope == SettingsScope.Plugin &&
string.Equals(section.SubjectId, pluginId, StringComparison.OrdinalIgnoreCase));
}
}
}

View File

@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services.Settings;
public enum WallpaperMediaType
{
None,
Image,
Video
}
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
public sealed record WallpaperSettingsState(string? WallpaperPath, string Placement);
public sealed record ThemeAppearanceSettingsState(bool IsNightMode, string? ThemeColor);
public sealed record StatusBarSettingsState(
IReadOnlyList<string> TopStatusComponentIds,
IReadOnlyList<string> PinnedTaskbarActions,
bool EnableDynamicTaskbarActions,
string TaskbarLayoutMode,
string ClockDisplayFormat,
string SpacingMode,
int CustomSpacingPercent);
public sealed record WeatherSettingsState(
string LocationMode,
string LocationKey,
string LocationName,
double Latitude,
double Longitude,
bool AutoRefreshLocation,
string ExcludedAlerts,
string IconPackId,
bool NoTlsRequests,
string LocationQuery);
public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId);
public sealed record UpdateSettingsState(bool AutoCheckUpdates, bool IncludePrereleaseUpdates, string UpdateChannel);
public sealed record PluginManagementSettingsState(IReadOnlyList<string> DisabledPluginIds);
public sealed record PluginMarketPluginInfo(
string Id,
string Name,
string Description,
string Author,
string Version,
string ApiVersion,
string MinHostVersion,
string DownloadUrl,
string ReleaseTag,
string ReleaseAssetName,
string IconUrl,
string ReadmeUrl,
string HomepageUrl,
string RepositoryUrl,
IReadOnlyList<string> Tags,
DateTimeOffset PublishedAt,
DateTimeOffset UpdatedAt);
public sealed record PluginMarketIndexResult(
bool Success,
IReadOnlyList<PluginMarketPluginInfo> Plugins,
string? Source,
string? SourceLocation,
string? WarningMessage,
string? ErrorMessage);
public sealed record PluginMarketInstallResult(
bool Success,
string? PluginId,
string? PluginName,
string? ErrorMessage);
public interface IGridSettingsService
{
GridSettingsState Get();
void Save(GridSettingsState state);
}
public interface IWallpaperSettingsService
{
WallpaperSettingsState Get();
void Save(WallpaperSettingsState state);
}
public interface IWallpaperMediaService
{
WallpaperMediaType DetectMediaType(string? path);
Task<string?> ImportAssetAsync(string sourcePath, CancellationToken cancellationToken = default);
}
public interface IThemeAppearanceService
{
ThemeAppearanceSettingsState Get();
void Save(ThemeAppearanceSettingsState state);
MonetPalette BuildPalette(bool nightMode, string? wallpaperPath);
}
public interface IStatusBarSettingsService
{
StatusBarSettingsState Get();
void Save(StatusBarSettingsState state);
}
public interface IWeatherProvider
{
Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
string keyword,
string? locale = null,
CancellationToken cancellationToken = default);
Task<WeatherQueryResult<WeatherSnapshot>> GetWeatherAsync(
WeatherQuery query,
CancellationToken cancellationToken = default);
}
public interface IWeatherSettingsService
{
WeatherSettingsState Get();
void Save(WeatherSettingsState state);
}
public interface IRegionSettingsService
{
RegionSettingsState Get();
void Save(RegionSettingsState state);
}
public interface IUpdateSettingsService
{
UpdateSettingsState Get();
void Save(UpdateSettingsState state);
Task<UpdateCheckResult> CheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default);
Task<UpdateDownloadResult> DownloadAssetAsync(
GitHubReleaseAsset asset,
string destinationFilePath,
IProgress<double>? progress = null,
CancellationToken cancellationToken = default);
}
public interface ILauncherCatalogService
{
StartMenuFolderNode LoadCatalog();
}
public interface ILauncherPolicyService
{
LauncherSettingsSnapshot Get();
void Save(LauncherSettingsSnapshot snapshot);
}
public interface IPluginManagementSettingsService
{
PluginManagementSettingsState Get();
void Save(PluginManagementSettingsState state);
IReadOnlyList<InstalledPluginInfo> GetInstalledPlugins();
bool SetPluginEnabled(string pluginId, bool isEnabled);
bool DeleteInstalledPlugin(string pluginId);
}
public interface IPluginMarketSettingsService
{
Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default);
Task<PluginMarketInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
}
public interface IApplicationInfoService
{
string GetAppVersionText();
AppRenderBackendInfo GetRenderBackendInfo();
}
public interface ISettingsFacadeService
{
ISettingsService Settings { get; }
ISettingsCatalog Catalog { get; }
IGridSettingsService Grid { get; }
IWallpaperSettingsService Wallpaper { get; }
IWallpaperMediaService WallpaperMedia { get; }
IThemeAppearanceService Theme { get; }
IStatusBarSettingsService StatusBar { get; }
IWeatherSettingsService Weather { get; }
IRegionSettingsService Region { get; }
IUpdateSettingsService Update { get; }
ILauncherCatalogService LauncherCatalog { get; }
ILauncherPolicyService LauncherPolicy { get; }
IPluginManagementSettingsService PluginManagement { get; }
IPluginMarketSettingsService PluginMarket { get; }
IApplicationInfoService ApplicationInfo { get; }
}

View File

@@ -0,0 +1,627 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Services.Settings;
internal sealed class GridSettingsService : IGridSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
public GridSettingsState Get()
{
var snapshot = _appSettingsService.Load();
return new GridSettingsState(
snapshot.GridShortSideCells,
snapshot.GridSpacingPreset,
snapshot.DesktopEdgeInsetPercent);
}
public void Save(GridSettingsState state)
{
var snapshot = _appSettingsService.Load();
snapshot.GridShortSideCells = state.ShortSideCells;
snapshot.GridSpacingPreset = state.SpacingPreset;
snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent;
_appSettingsService.Save(snapshot);
}
}
internal sealed class WallpaperSettingsService : IWallpaperSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
public WallpaperSettingsState Get()
{
var snapshot = _appSettingsService.Load();
return new WallpaperSettingsState(snapshot.WallpaperPath, snapshot.WallpaperPlacement);
}
public void Save(WallpaperSettingsState state)
{
var snapshot = _appSettingsService.Load();
snapshot.WallpaperPath = state.WallpaperPath;
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
? "Fill"
: state.Placement.Trim();
_appSettingsService.Save(snapshot);
}
}
internal sealed class WallpaperMediaService : IWallpaperMediaService
{
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp"
};
private static readonly HashSet<string> VideoExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v"
};
private readonly string _wallpapersDirectory;
public WallpaperMediaService()
{
var appDataRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
_wallpapersDirectory = Path.Combine(appDataRoot, "Wallpapers");
}
public WallpaperMediaType DetectMediaType(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return WallpaperMediaType.None;
}
var extension = Path.GetExtension(path.Trim());
if (string.IsNullOrWhiteSpace(extension))
{
return WallpaperMediaType.None;
}
if (ImageExtensions.Contains(extension))
{
return WallpaperMediaType.Image;
}
if (VideoExtensions.Contains(extension))
{
return WallpaperMediaType.Video;
}
return WallpaperMediaType.None;
}
public async Task<string?> ImportAssetAsync(string sourcePath, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(sourcePath))
{
return null;
}
var fullSourcePath = Path.GetFullPath(sourcePath);
if (!File.Exists(fullSourcePath))
{
return null;
}
if (DetectMediaType(fullSourcePath) == WallpaperMediaType.None)
{
return null;
}
Directory.CreateDirectory(_wallpapersDirectory);
var extension = Path.GetExtension(fullSourcePath);
var baseName = Path.GetFileNameWithoutExtension(fullSourcePath);
var normalizedBaseName = string.IsNullOrWhiteSpace(baseName)
? "wallpaper"
: string.Concat(baseName.Select(ch => Path.GetInvalidFileNameChars().Contains(ch) ? '_' : ch));
var destinationPath = Path.Combine(_wallpapersDirectory, $"{normalizedBaseName}{extension}");
if (string.Equals(fullSourcePath, destinationPath, StringComparison.OrdinalIgnoreCase))
{
return destinationPath;
}
var suffix = 1;
while (File.Exists(destinationPath))
{
destinationPath = Path.Combine(_wallpapersDirectory, $"{normalizedBaseName}_{suffix}{extension}");
suffix++;
}
await using var source = File.OpenRead(fullSourcePath);
await using var destination = File.Create(destinationPath);
await source.CopyToAsync(destination, cancellationToken);
return destinationPath;
}
}
internal sealed class ThemeAppearanceService : IThemeAppearanceService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly MonetColorService _monetColorService = new();
private readonly WallpaperMediaService _wallpaperMediaService = new();
public ThemeAppearanceSettingsState Get()
{
var snapshot = _appSettingsService.Load();
return new ThemeAppearanceSettingsState(
snapshot.IsNightMode ?? false,
snapshot.ThemeColor);
}
public void Save(ThemeAppearanceSettingsState state)
{
var snapshot = _appSettingsService.Load();
snapshot.IsNightMode = state.IsNightMode;
snapshot.ThemeColor = state.ThemeColor;
_appSettingsService.Save(snapshot);
}
public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath)
{
Bitmap? bitmap = null;
try
{
if (_wallpaperMediaService.DetectMediaType(wallpaperPath) == WallpaperMediaType.Image &&
!string.IsNullOrWhiteSpace(wallpaperPath) &&
File.Exists(wallpaperPath))
{
bitmap = new Bitmap(wallpaperPath);
}
}
catch (Exception ex)
{
AppLogger.Warn(
"Settings.Theme",
$"Failed to load wallpaper bitmap for palette generation. Path='{wallpaperPath}'.",
ex);
}
try
{
return _monetColorService.BuildPalette(bitmap, nightMode);
}
finally
{
bitmap?.Dispose();
}
}
}
internal sealed class StatusBarSettingsService : IStatusBarSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
public StatusBarSettingsState Get()
{
var snapshot = _appSettingsService.Load();
return new StatusBarSettingsState(
snapshot.TopStatusComponentIds?.ToArray() ?? [],
snapshot.PinnedTaskbarActions?.ToArray() ?? [],
snapshot.EnableDynamicTaskbarActions,
snapshot.TaskbarLayoutMode,
snapshot.ClockDisplayFormat,
snapshot.StatusBarSpacingMode,
snapshot.StatusBarCustomSpacingPercent);
}
public void Save(StatusBarSettingsState state)
{
var snapshot = _appSettingsService.Load();
snapshot.TopStatusComponentIds = state.TopStatusComponentIds?.ToList() ?? [];
snapshot.PinnedTaskbarActions = state.PinnedTaskbarActions?.ToList() ?? [];
snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions;
snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode;
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
snapshot.StatusBarSpacingMode = state.SpacingMode;
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
_appSettingsService.Save(snapshot);
}
}
internal sealed class WeatherProviderAdapter : IWeatherProvider
{
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
public Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
string keyword,
string? locale = null,
CancellationToken cancellationToken = default)
{
return _weatherDataService.SearchLocationsAsync(keyword, locale, cancellationToken);
}
public Task<WeatherQueryResult<WeatherSnapshot>> GetWeatherAsync(
WeatherQuery query,
CancellationToken cancellationToken = default)
{
return _weatherDataService.GetWeatherAsync(query, cancellationToken);
}
}
internal sealed class WeatherSettingsService : IWeatherSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
public WeatherSettingsState Get()
{
var snapshot = _appSettingsService.Load();
return new WeatherSettingsState(
snapshot.WeatherLocationMode,
snapshot.WeatherLocationKey,
snapshot.WeatherLocationName,
snapshot.WeatherLatitude,
snapshot.WeatherLongitude,
snapshot.WeatherAutoRefreshLocation,
snapshot.WeatherExcludedAlerts,
snapshot.WeatherIconPackId,
snapshot.WeatherNoTlsRequests,
snapshot.WeatherLocationQuery);
}
public void Save(WeatherSettingsState state)
{
var snapshot = _appSettingsService.Load();
snapshot.WeatherLocationMode = state.LocationMode;
snapshot.WeatherLocationKey = state.LocationKey;
snapshot.WeatherLocationName = state.LocationName;
snapshot.WeatherLatitude = state.Latitude;
snapshot.WeatherLongitude = state.Longitude;
snapshot.WeatherAutoRefreshLocation = state.AutoRefreshLocation;
snapshot.WeatherExcludedAlerts = state.ExcludedAlerts;
snapshot.WeatherIconPackId = state.IconPackId;
snapshot.WeatherNoTlsRequests = state.NoTlsRequests;
snapshot.WeatherLocationQuery = state.LocationQuery;
_appSettingsService.Save(snapshot);
}
}
internal sealed class RegionSettingsService : IRegionSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
public RegionSettingsState Get()
{
var snapshot = _appSettingsService.Load();
return new RegionSettingsState(snapshot.LanguageCode, snapshot.TimeZoneId);
}
public void Save(RegionSettingsState state)
{
var snapshot = _appSettingsService.Load();
snapshot.LanguageCode = string.IsNullOrWhiteSpace(state.LanguageCode)
? "zh-CN"
: state.LanguageCode.Trim();
snapshot.TimeZoneId = string.IsNullOrWhiteSpace(state.TimeZoneId)
? null
: state.TimeZoneId.Trim();
_appSettingsService.Save(snapshot);
}
}
internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable
{
private readonly AppSettingsService _appSettingsService = new();
private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
public UpdateSettingsState Get()
{
var snapshot = _appSettingsService.Load();
return new UpdateSettingsState(
snapshot.AutoCheckUpdates,
snapshot.IncludePrereleaseUpdates,
snapshot.UpdateChannel);
}
public void Save(UpdateSettingsState state)
{
var snapshot = _appSettingsService.Load();
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
snapshot.IncludePrereleaseUpdates = state.IncludePrereleaseUpdates;
snapshot.UpdateChannel = state.UpdateChannel;
_appSettingsService.Save(snapshot);
}
public Task<UpdateCheckResult> CheckForUpdatesAsync(
Version currentVersion,
bool includePrerelease,
CancellationToken cancellationToken = default)
{
return _releaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
}
public Task<UpdateDownloadResult> DownloadAssetAsync(
GitHubReleaseAsset asset,
string destinationFilePath,
IProgress<double>? progress = null,
CancellationToken cancellationToken = default)
{
return _releaseUpdateService.DownloadAssetAsync(asset, destinationFilePath, progress, cancellationToken);
}
public void Dispose()
{
_releaseUpdateService.Dispose();
}
}
internal sealed class LauncherCatalogService : ILauncherCatalogService
{
private readonly WindowsStartMenuService _startMenuService = new();
public StartMenuFolderNode LoadCatalog()
{
return _startMenuService.Load();
}
}
internal sealed class LauncherPolicyService : ILauncherPolicyService
{
private readonly LauncherSettingsService _launcherSettingsService = new();
public LauncherSettingsSnapshot Get()
{
return _launcherSettingsService.Load();
}
public void Save(LauncherSettingsSnapshot snapshot)
{
_launcherSettingsService.Save(snapshot ?? new LauncherSettingsSnapshot());
}
}
internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly PluginRuntimeService? _pluginRuntimeService;
public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService)
{
_pluginRuntimeService = pluginRuntimeService;
}
public PluginManagementSettingsState Get()
{
var snapshot = _appSettingsService.Load();
return new PluginManagementSettingsState(snapshot.DisabledPluginIds?.ToArray() ?? []);
}
public void Save(PluginManagementSettingsState state)
{
var snapshot = _appSettingsService.Load();
snapshot.DisabledPluginIds = state.DisabledPluginIds?.ToList() ?? [];
_appSettingsService.Save(snapshot);
}
public IReadOnlyList<InstalledPluginInfo> GetInstalledPlugins()
{
return _pluginRuntimeService?.GetInstalledPluginsSnapshot() ?? [];
}
public bool SetPluginEnabled(string pluginId, bool isEnabled)
{
return _pluginRuntimeService?.SetPluginEnabled(pluginId, isEnabled) ?? false;
}
public bool DeleteInstalledPlugin(string pluginId)
{
return _pluginRuntimeService?.DeleteInstalledPlugin(pluginId) ?? false;
}
}
internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService, IDisposable
{
private readonly PluginRuntimeService? _pluginRuntimeService;
private readonly AirAppMarketIndexService _indexService;
private readonly AirAppMarketInstallService? _installService;
private readonly Dictionary<string, AirAppMarketPluginEntry> _cachedPlugins = new(StringComparer.OrdinalIgnoreCase);
public PluginMarketSettingsService(PluginRuntimeService? pluginRuntimeService)
{
_pluginRuntimeService = pluginRuntimeService;
var dataRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop",
"PluginMarket");
var cacheService = new AirAppMarketCacheService(dataRoot);
_indexService = new AirAppMarketIndexService(cacheService);
if (_pluginRuntimeService is not null)
{
_installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot);
}
}
public async Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default)
{
var result = await _indexService.LoadAsync(cancellationToken);
if (!result.Success || result.Document is null)
{
return new PluginMarketIndexResult(
false,
[],
result.Source?.ToString(),
result.SourceLocation,
result.WarningMessage,
result.ErrorMessage);
}
_cachedPlugins.Clear();
var plugins = result.Document.Plugins
.Select(entry =>
{
_cachedPlugins[entry.Id] = entry;
return new PluginMarketPluginInfo(
entry.Id,
entry.Name,
entry.Description,
entry.Author,
entry.Version,
entry.ApiVersion,
entry.MinHostVersion,
entry.DownloadUrl,
entry.ReleaseTag,
entry.ReleaseAssetName,
entry.IconUrl,
entry.ReadmeUrl,
entry.HomepageUrl,
entry.RepositoryUrl,
entry.Tags,
entry.PublishedAt,
entry.UpdatedAt);
})
.ToArray();
return new PluginMarketIndexResult(
true,
plugins,
result.Source?.ToString(),
result.SourceLocation,
result.WarningMessage,
null);
}
public async Task<PluginMarketInstallResult> InstallAsync(
string pluginId,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(pluginId))
{
return new PluginMarketInstallResult(false, null, null, "Plugin id is required.");
}
if (_installService is null || _pluginRuntimeService is null)
{
return new PluginMarketInstallResult(
false,
pluginId,
null,
"Plugin runtime is unavailable.");
}
if (!_cachedPlugins.TryGetValue(pluginId, out var entry))
{
var load = await LoadIndexAsync(cancellationToken);
if (!load.Success)
{
return new PluginMarketInstallResult(false, pluginId, null, load.ErrorMessage);
}
if (!_cachedPlugins.TryGetValue(pluginId, out entry))
{
return new PluginMarketInstallResult(false, pluginId, null, "Plugin was not found in market index.");
}
}
var result = await _installService.InstallAsync(entry, cancellationToken);
if (!result.Success)
{
return new PluginMarketInstallResult(false, entry.Id, entry.Name, result.ErrorMessage);
}
return new PluginMarketInstallResult(true, result.Manifest?.Id ?? entry.Id, result.Manifest?.Name ?? entry.Name, null);
}
public void Dispose()
{
_indexService.Dispose();
_installService?.Dispose();
}
}
internal sealed class ApplicationInfoService : IApplicationInfoService
{
public string GetAppVersionText()
{
var version = typeof(App).Assembly.GetName().Version;
return version is null
? "0.0.0"
: new Version(
Math.Max(0, version.Major),
Math.Max(0, version.Minor),
Math.Max(0, version.Build)).ToString(3);
}
public AppRenderBackendInfo GetRenderBackendInfo()
{
return AppRenderBackendDiagnostics.Detect();
}
}
internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposable
{
private readonly UpdateSettingsService _updateSettingsService;
private readonly PluginMarketSettingsService _pluginMarketSettingsService;
public SettingsFacadeService(PluginRuntimeService? pluginRuntimeService = null)
{
Settings = new SettingsService();
Catalog = new SettingsCatalogService();
Grid = new GridSettingsService();
Wallpaper = new WallpaperSettingsService();
WallpaperMedia = new WallpaperMediaService();
Theme = new ThemeAppearanceService();
StatusBar = new StatusBarSettingsService();
Weather = new WeatherSettingsService();
Region = new RegionSettingsService();
_updateSettingsService = new UpdateSettingsService();
Update = _updateSettingsService;
LauncherCatalog = new LauncherCatalogService();
LauncherPolicy = new LauncherPolicyService();
PluginManagement = new PluginManagementSettingsService(pluginRuntimeService);
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
PluginMarket = _pluginMarketSettingsService;
ApplicationInfo = new ApplicationInfoService();
}
public ISettingsService Settings { get; }
public ISettingsCatalog Catalog { get; }
public IGridSettingsService Grid { get; }
public IWallpaperSettingsService Wallpaper { get; }
public IWallpaperMediaService WallpaperMedia { get; }
public IThemeAppearanceService Theme { get; }
public IStatusBarSettingsService StatusBar { get; }
public IWeatherSettingsService Weather { get; }
public IRegionSettingsService Region { get; }
public IUpdateSettingsService Update { get; }
public ILauncherCatalogService LauncherCatalog { get; }
public ILauncherPolicyService LauncherPolicy { get; }
public IPluginManagementSettingsService PluginManagement { get; }
public IPluginMarketSettingsService PluginMarket { get; }
public IApplicationInfoService ApplicationInfo { get; }
public void Dispose()
{
_updateSettingsService.Dispose();
_pluginMarketSettingsService.Dispose();
}
}

View File

@@ -0,0 +1,423 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services.Settings;
internal sealed class SettingsService : ISettingsService
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
private readonly AppSettingsService _appSettingsService = new();
private readonly LauncherSettingsService _launcherSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly string _pluginSettingsPath;
private readonly object _pluginSettingsGate = new();
public SettingsService()
{
var root = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
_pluginSettingsPath = Path.Combine(root, "plugin-settings.json");
}
public event EventHandler<SettingsChangedEvent>? Changed;
public T LoadSnapshot<T>(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new()
{
return scope switch
{
SettingsScope.App => ConvertSnapshot<AppSettingsSnapshot, T>(_appSettingsService.Load()),
SettingsScope.Launcher => ConvertSnapshot<LauncherSettingsSnapshot, T>(_launcherSettingsService.Load()),
SettingsScope.ComponentInstance => LoadComponentSnapshot<T>(subjectId, placementId),
SettingsScope.Plugin => LoadSection<T>(scope, EnsureKey(subjectId), sectionId: "__snapshot__", placementId),
_ => new T()
};
}
public void SaveSnapshot<T>(
SettingsScope scope,
T snapshot,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
switch (scope)
{
case SettingsScope.App:
_appSettingsService.Save(ConvertSnapshot<T, AppSettingsSnapshot>(snapshot));
break;
case SettingsScope.Launcher:
_launcherSettingsService.Save(ConvertSnapshot<T, LauncherSettingsSnapshot>(snapshot));
break;
case SettingsScope.ComponentInstance:
SaveComponentSnapshot(subjectId, placementId, snapshot);
break;
case SettingsScope.Plugin:
SaveSection(scope, EnsureKey(subjectId), "__snapshot__", snapshot, placementId, changedKeys);
break;
}
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
}
public T LoadSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null) where T : new()
{
if (scope == SettingsScope.ComponentInstance)
{
return _componentSettingsService.LoadPluginSettings<T>(EnsureKey(subjectId), placementId);
}
if (scope != SettingsScope.Plugin)
{
return new T();
}
lock (_pluginSettingsGate)
{
var document = LoadPluginDocumentLocked();
if (!document.Sections.TryGetValue(EnsureKey(subjectId), out var pluginSections) ||
!pluginSections.TryGetValue(EnsureKey(sectionId), out var payload))
{
return new T();
}
return JsonSerializer.Deserialize<T>(payload.GetRawText(), SerializerOptions) ?? new T();
}
}
public void SaveSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
T section,
string? placementId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
if (scope == SettingsScope.ComponentInstance)
{
_componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, section);
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
return;
}
if (scope != SettingsScope.Plugin)
{
return;
}
lock (_pluginSettingsGate)
{
var document = LoadPluginDocumentLocked();
var pluginId = EnsureKey(subjectId);
if (!document.Sections.TryGetValue(pluginId, out var pluginSections))
{
pluginSections = new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
document.Sections[pluginId] = pluginSections;
}
pluginSections[EnsureKey(sectionId)] = JsonSerializer.SerializeToElement(section, SerializerOptions).Clone();
PersistPluginDocumentLocked(document);
}
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
}
public void DeleteSection(SettingsScope scope, string subjectId, string sectionId, string? placementId = null)
{
if (scope == SettingsScope.ComponentInstance)
{
_componentSettingsService.DeletePluginSettings(EnsureKey(subjectId), placementId);
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId));
return;
}
if (scope != SettingsScope.Plugin)
{
return;
}
lock (_pluginSettingsGate)
{
var document = LoadPluginDocumentLocked();
var pluginId = EnsureKey(subjectId);
if (document.Sections.TryGetValue(pluginId, out var sections) &&
sections.Remove(EnsureKey(sectionId)))
{
if (sections.Count == 0)
{
document.Sections.Remove(pluginId);
}
PersistPluginDocumentLocked(document);
}
}
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId));
}
public T? GetValue<T>(
SettingsScope scope,
string key,
string? subjectId = null,
string? placementId = null,
string? sectionId = null)
{
var snapshot = scope switch
{
SettingsScope.App => JsonSerializer.SerializeToElement(_appSettingsService.Load(), SerializerOptions),
SettingsScope.Launcher => JsonSerializer.SerializeToElement(_launcherSettingsService.Load(), SerializerOptions),
SettingsScope.ComponentInstance => JsonSerializer.SerializeToElement(
_componentSettingsService.LoadForComponent(EnsureKey(subjectId), placementId),
SerializerOptions),
SettingsScope.Plugin => JsonSerializer.SerializeToElement(
LoadSection<Dictionary<string, JsonElement>>(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", placementId),
SerializerOptions),
_ => default
};
if (snapshot.ValueKind != JsonValueKind.Object)
{
return default;
}
foreach (var property in snapshot.EnumerateObject())
{
if (!string.Equals(property.Name, key, StringComparison.OrdinalIgnoreCase))
{
continue;
}
try
{
return property.Value.Deserialize<T>(SerializerOptions);
}
catch
{
return default;
}
}
return default;
}
public void SetValue<T>(
SettingsScope scope,
string key,
T value,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
if (scope == SettingsScope.Plugin)
{
var dict = LoadSection<Dictionary<string, JsonElement>>(
SettingsScope.Plugin,
EnsureKey(subjectId),
sectionId ?? "__root__",
placementId);
dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone();
SaveSection(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", dict, placementId, changedKeys ?? [key]);
return;
}
if (scope == SettingsScope.ComponentInstance)
{
var dict = _componentSettingsService.LoadPluginSettings<Dictionary<string, JsonElement>>(EnsureKey(subjectId), placementId);
dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone();
_componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, dict);
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys ?? [key]));
return;
}
if (scope == SettingsScope.App)
{
var snapshot = _appSettingsService.Load();
var updated = UpdateObjectKey(snapshot, key, value);
_appSettingsService.Save(updated);
OnChanged(new SettingsChangedEvent(scope, null, null, sectionId, changedKeys ?? [key]));
return;
}
if (scope == SettingsScope.Launcher)
{
var snapshot = _launcherSettingsService.Load();
var updated = UpdateObjectKey(snapshot, key, value);
_launcherSettingsService.Save(updated);
OnChanged(new SettingsChangedEvent(scope, null, null, sectionId, changedKeys ?? [key]));
}
}
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
{
return new ComponentSettingsAccessor(this, componentId, placementId);
}
private T LoadComponentSnapshot<T>(string? componentId, string? placementId) where T : new()
{
var snapshot = _componentSettingsService.LoadForComponent(EnsureKey(componentId), placementId);
return ConvertSnapshot<ComponentSettingsSnapshot, T>(snapshot);
}
private void SaveComponentSnapshot<T>(string? componentId, string? placementId, T snapshot)
{
var converted = ConvertSnapshot<T, ComponentSettingsSnapshot>(snapshot);
_componentSettingsService.SaveForComponent(EnsureKey(componentId), placementId, converted);
}
private static TOut ConvertSnapshot<TIn, TOut>(TIn source) where TOut : new()
{
if (source is null)
{
return new TOut();
}
if (source is TOut direct)
{
return direct;
}
try
{
var json = JsonSerializer.Serialize(source, SerializerOptions);
return JsonSerializer.Deserialize<TOut>(json, SerializerOptions) ?? new TOut();
}
catch
{
return new TOut();
}
}
private static TSnapshot UpdateObjectKey<TSnapshot, TValue>(TSnapshot snapshot, string key, TValue value)
where TSnapshot : new()
{
var bag = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
JsonSerializer.Serialize(snapshot, SerializerOptions),
SerializerOptions) ?? new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
var actualKey = bag.Keys.FirstOrDefault(existing => string.Equals(existing, key, StringComparison.OrdinalIgnoreCase)) ?? key;
bag[actualKey] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone();
try
{
var json = JsonSerializer.Serialize(bag, SerializerOptions);
return JsonSerializer.Deserialize<TSnapshot>(json, SerializerOptions) ?? new TSnapshot();
}
catch
{
return snapshot is null ? new TSnapshot() : snapshot;
}
}
private PluginSettingsDocument LoadPluginDocumentLocked()
{
try
{
if (!File.Exists(_pluginSettingsPath))
{
return new PluginSettingsDocument();
}
var json = File.ReadAllText(_pluginSettingsPath);
return JsonSerializer.Deserialize<PluginSettingsDocument>(json, SerializerOptions) ?? new PluginSettingsDocument();
}
catch (Exception ex)
{
AppLogger.Warn("SettingsService", $"Failed to load plugin settings '{_pluginSettingsPath}'.", ex);
return new PluginSettingsDocument();
}
}
private void PersistPluginDocumentLocked(PluginSettingsDocument document)
{
try
{
var directory = Path.GetDirectoryName(_pluginSettingsPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(_pluginSettingsPath, JsonSerializer.Serialize(document, SerializerOptions));
}
catch (Exception ex)
{
AppLogger.Warn("SettingsService", $"Failed to persist plugin settings '{_pluginSettingsPath}'.", ex);
}
}
private static string EnsureKey(string? value)
{
return string.IsNullOrWhiteSpace(value) ? "__default__" : value.Trim();
}
private void OnChanged(SettingsChangedEvent e)
{
try
{
Changed?.Invoke(this, e);
}
catch
{
// Never let a subscriber break settings persistence.
}
}
private sealed class ComponentSettingsAccessor : IComponentSettingsAccessor
{
private readonly SettingsService _settingsService;
public ComponentSettingsAccessor(SettingsService settingsService, string componentId, string? placementId)
{
_settingsService = settingsService;
ComponentId = componentId;
PlacementId = placementId;
}
public string ComponentId { get; }
public string? PlacementId { get; }
public T LoadSnapshot<T>() where T : new()
=> _settingsService.LoadSnapshot<T>(SettingsScope.ComponentInstance, ComponentId, PlacementId);
public void SaveSnapshot<T>(T snapshot, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SaveSnapshot(SettingsScope.ComponentInstance, snapshot, ComponentId, PlacementId, changedKeys: changedKeys);
public T LoadSection<T>(string sectionId) where T : new()
=> _settingsService.LoadSection<T>(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId);
public void SaveSection<T>(string sectionId, T section, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SaveSection(SettingsScope.ComponentInstance, ComponentId, sectionId, section, PlacementId, changedKeys);
public void DeleteSection(string sectionId)
=> _settingsService.DeleteSection(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId);
public T? GetValue<T>(string key)
=> _settingsService.GetValue<T>(SettingsScope.ComponentInstance, key, ComponentId, PlacementId);
public void SetValue<T>(string key, T value, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SetValue(SettingsScope.ComponentInstance, key, value, ComponentId, PlacementId, changedKeys: changedKeys);
}
private sealed class PluginSettingsDocument
{
public Dictionary<string, Dictionary<string, JsonElement>> Sections { get; set; } = new(StringComparer.OrdinalIgnoreCase);
}
}

View File

@@ -1,73 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="560"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.AnalogClockWidgetSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="时钟设置"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="为单时钟选择时区。"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="TimeZoneLabelTextBlock"
Text="时区"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="TimeZoneComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnTimeZoneSelectionChanged" />
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="SecondHandModeLabelTextBlock"
Text="秒针方式"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<StackPanel Orientation="Horizontal"
Spacing="12">
<RadioButton x:Name="SecondHandTickRadioButton"
GroupName="desktop_clock_second_mode"
Content="跳针"
Checked="OnSecondHandModeChanged" />
<RadioButton x:Name="SecondHandSweepRadioButton"
GroupName="desktop_clock_second_mode"
Content="扫针"
Checked="OnSecondHandModeChanged" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,230 +0,0 @@
using System;
using System.Collections.Generic;
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, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IReadOnlyDictionary<string, string> ZhTimeZoneNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["China Standard Time"] = "中国标准时间",
["Asia/Shanghai"] = "中国标准时间",
["GMT Standard Time"] = "格林威治标准时间",
["Europe/London"] = "格林威治标准时间",
["AUS Eastern Standard Time"] = "澳大利亚东部标准时间",
["Australia/Sydney"] = "澳大利亚东部标准时间",
["Eastern Standard Time"] = "美国东部标准时间",
["America/New_York"] = "美国东部标准时间",
["Tokyo Standard Time"] = "日本标准时间",
["Asia/Tokyo"] = "日本标准时间",
["UTC"] = "协调世界时",
["Etc/UTC"] = "协调世界时"
};
private readonly AppSettingsService _appSettingsService = 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;
public event EventHandler? SettingsChanged;
public AnalogClockWidgetSettingsWindow()
{
InitializeComponent();
LoadState();
ApplyLocalization();
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 = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
_selectedTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId)
? "China Standard Time"
: componentSnapshot.DesktopClockTimeZoneId.Trim();
_secondHandMode = ClockSecondHandMode.Normalize(componentSnapshot.DesktopClockSecondHandMode);
_allTimeZones = _timeZoneService
.GetAllTimeZones()
.OrderBy(zone => zone.GetUtcOffset(DateTime.UtcNow))
.ThenBy(zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("desktop_clock.settings.title", "时钟设置");
DescriptionTextBlock.Text = L("desktop_clock.settings.desc", "为单时钟选择时区。");
TimeZoneLabelTextBlock.Text = L("desktop_clock.settings.timezone_label", "时区");
SecondHandModeLabelTextBlock.Text = L("desktop_clock.settings.second_mode_label", "秒针方式");
SecondHandTickRadioButton.Content = L("clock.second_mode.tick", "跳针");
SecondHandSweepRadioButton.Content = L("clock.second_mode.sweep", "扫针");
}
private void PopulateTimeZoneComboBox()
{
_suppressEvents = true;
try
{
TimeZoneComboBox.Items.Clear();
foreach (var timeZone in _allTimeZones)
{
TimeZoneComboBox.Items.Add(new ComboBoxItem
{
Tag = timeZone.Id,
Content = GetLocalizedTimeZoneDisplayName(timeZone)
});
}
var normalizedId = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(
new[] { _selectedTimeZoneId },
_allTimeZones)[0];
_selectedTimeZoneId = normalizedId;
var selected = TimeZoneComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item => string.Equals(item.Tag as string, normalizedId, StringComparison.OrdinalIgnoreCase));
TimeZoneComboBox.SelectedItem = selected ?? TimeZoneComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
var normalizedMode = ClockSecondHandMode.Normalize(_secondHandMode);
SecondHandTickRadioButton.IsChecked = string.Equals(
normalizedMode,
ClockSecondHandMode.Tick,
StringComparison.OrdinalIgnoreCase);
SecondHandSweepRadioButton.IsChecked = string.Equals(
normalizedMode,
ClockSecondHandMode.Sweep,
StringComparison.OrdinalIgnoreCase);
}
finally
{
_suppressEvents = false;
}
}
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void OnSecondHandModeChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var selectedId = (TimeZoneComboBox.SelectedItem as ComboBoxItem)?.Tag as string;
var normalizedId = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(
new[] { selectedId ?? _selectedTimeZoneId },
_allTimeZones)[0];
_selectedTimeZoneId = normalizedId;
_secondHandMode = GetSelectedSecondHandMode();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.DesktopClockTimeZoneId = normalizedId;
snapshot.DesktopClockSecondHandMode = _secondHandMode;
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string GetSelectedSecondHandMode()
{
return SecondHandSweepRadioButton.IsChecked == true
? ClockSecondHandMode.Sweep
: ClockSecondHandMode.Tick;
}
private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone)
{
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
var sign = offset >= TimeSpan.Zero ? "+" : "-";
var totalMinutes = Math.Abs((int)offset.TotalMinutes);
var hours = totalMinutes / 60;
var minutes = totalMinutes % 60;
var displayName = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
? ResolveZhDisplayName(timeZone)
: ResolveEnDisplayName(timeZone);
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {displayName}";
}
private static string ResolveZhDisplayName(TimeZoneInfo timeZone)
{
if (ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName))
{
return localizedName;
}
return string.IsNullOrWhiteSpace(timeZone.StandardName)
? timeZone.DisplayName
: timeZone.StandardName;
}
private static string ResolveEnDisplayName(TimeZoneInfo timeZone)
{
if (!string.IsNullOrWhiteSpace(timeZone.StandardName))
{
return timeZone.StandardName;
}
return timeZone.DisplayName;
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,110 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.BaiduHotSearchSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="Baidu hot search settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure source, auto refresh and refresh interval."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="SourceLabelTextBlock"
Text="Data source"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="SourceComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnSourceSelectionChanged">
<ComboBoxItem x:Name="SourceOfficialItem"
Tag="Official"
Content="Official Source" />
<ComboBoxItem x:Name="SourceThirdPartyRssItem"
Tag="ThirdPartyRss"
Content="Third-party RSS" />
</ComboBox>
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRefreshLabelTextBlock"
Text="Auto refresh"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRefreshCheckBox"
Content="Enable auto refresh"
Checked="OnAutoRefreshChanged"
Unchecked="OnAutoRefreshChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Refresh interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency5mItem"
Tag="5"
Content="5 min" />
<ComboBoxItem x:Name="Frequency10mItem"
Tag="10"
Content="10 min" />
<ComboBoxItem x:Name="Frequency15mItem"
Tag="15"
Content="15 min" />
<ComboBoxItem x:Name="Frequency30mItem"
Tag="30"
Content="30 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
<ComboBoxItem x:Name="Frequency3hItem"
Tag="180"
Content="3 hours" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,193 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class BaiduHotSearchSettingsWindow : UserControl
{
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private bool _suppressEvents;
private string _languageCode = "zh-CN";
public event EventHandler? SettingsChanged;
public BaiduHotSearchSettingsWindow()
{
InitializeComponent();
InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var sourceType = BaiduHotSearchSourceTypes.Normalize(componentSnapshot.BaiduHotSearchSourceType);
var enabled = componentSnapshot.BaiduHotSearchAutoRefreshEnabled;
var interval = NormalizeInterval(componentSnapshot.BaiduHotSearchAutoRefreshIntervalMinutes);
_suppressEvents = true;
SelectSourceType(sourceType);
AutoRefreshCheckBox.IsChecked = enabled;
SelectInterval(interval);
FrequencyCardBorder.IsVisible = enabled;
_suppressEvents = false;
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("baiduhot.settings.title", "Baidu hot search settings");
DescriptionTextBlock.Text = L("baiduhot.settings.desc", "Configure source, auto refresh and refresh interval.");
SourceLabelTextBlock.Text = L("baiduhot.settings.source_label", "Data source");
SourceOfficialItem.Content = L("baiduhot.settings.source_official", "Official Source");
SourceThirdPartyRssItem.Content = L("baiduhot.settings.source_rss", "Third-party RSS");
AutoRefreshLabelTextBlock.Text = L("baiduhot.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("baiduhot.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("baiduhot.settings.frequency_label", "Refresh interval");
ApplyFrequencyLocalization();
}
private void OnSourceSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var enabled = AutoRefreshCheckBox.IsChecked == true;
FrequencyCardBorder.IsVisible = enabled;
SaveState();
}
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var snapshot = _componentSettingsService.Load();
snapshot.BaiduHotSearchSourceType = GetSelectedSourceType();
snapshot.BaiduHotSearchAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
snapshot.BaiduHotSearchAutoRefreshIntervalMinutes = GetSelectedInterval();
_componentSettingsService.Save(snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string GetSelectedSourceType()
{
if (SourceComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string sourceTag)
{
return BaiduHotSearchSourceTypes.Normalize(sourceTag);
}
return BaiduHotSearchSourceTypes.Official;
}
private int GetSelectedInterval()
{
if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes))
{
return NormalizeInterval(minutes);
}
return 15;
}
private void SelectSourceType(string sourceType)
{
var normalizedSourceType = BaiduHotSearchSourceTypes.Normalize(sourceType);
var selected = SourceComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string sourceTag &&
string.Equals(BaiduHotSearchSourceTypes.Normalize(sourceTag), normalizedSourceType, StringComparison.OrdinalIgnoreCase));
SourceComboBox.SelectedItem = selected ?? SourceComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private void SelectInterval(int intervalMinutes)
{
var selected = FrequencyComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private static int NormalizeInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 15);
}
private void InitializeFrequencyOptions()
{
FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
}
private void ApplyFrequencyLocalization()
{
foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
if (item.Tag is not string tagText ||
!int.TryParse(tagText, out var minutes))
{
continue;
}
var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
}
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,87 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.BilibiliHotSearchSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="Bilibili hot search settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure auto refresh and refresh interval."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRefreshLabelTextBlock"
Text="Auto refresh"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRefreshCheckBox"
Content="Enable auto refresh"
Checked="OnAutoRefreshChanged"
Unchecked="OnAutoRefreshChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Refresh interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency5mItem"
Tag="5"
Content="5 min" />
<ComboBoxItem x:Name="Frequency10mItem"
Tag="10"
Content="10 min" />
<ComboBoxItem x:Name="Frequency15mItem"
Tag="15"
Content="15 min" />
<ComboBoxItem x:Name="Frequency30mItem"
Tag="30"
Content="30 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
<ComboBoxItem x:Name="Frequency3hItem"
Tag="180"
Content="3 hours" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,153 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class BilibiliHotSearchSettingsWindow : UserControl
{
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private bool _suppressEvents;
private string _languageCode = "zh-CN";
public event EventHandler? SettingsChanged;
public BilibiliHotSearchSettingsWindow()
{
InitializeComponent();
InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var enabled = componentSnapshot.BilibiliHotSearchAutoRefreshEnabled;
var interval = NormalizeInterval(componentSnapshot.BilibiliHotSearchAutoRefreshIntervalMinutes);
_suppressEvents = true;
AutoRefreshCheckBox.IsChecked = enabled;
SelectInterval(interval);
FrequencyCardBorder.IsVisible = enabled;
_suppressEvents = false;
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("bilihot.settings.title", "Bilibili hot search settings");
DescriptionTextBlock.Text = L("bilihot.settings.desc", "Configure auto refresh and refresh interval.");
AutoRefreshLabelTextBlock.Text = L("bilihot.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("bilihot.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("bilihot.settings.frequency_label", "Refresh interval");
ApplyFrequencyLocalization();
}
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var enabled = AutoRefreshCheckBox.IsChecked == true;
FrequencyCardBorder.IsVisible = enabled;
SaveState();
}
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var snapshot = _componentSettingsService.Load();
snapshot.BilibiliHotSearchAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
snapshot.BilibiliHotSearchAutoRefreshIntervalMinutes = GetSelectedInterval();
_componentSettingsService.Save(snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private int GetSelectedInterval()
{
if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes))
{
return NormalizeInterval(minutes);
}
return 15;
}
private void SelectInterval(int intervalMinutes)
{
var selected = FrequencyComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private static int NormalizeInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 15);
}
private void InitializeFrequencyOptions()
{
FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
}
private void ApplyFrequencyLocalization()
{
foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
if (item.Tag is not string tagText ||
!int.TryParse(tagText, out var minutes))
{
continue;
}
var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
}
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,61 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="520"
d:DesignHeight="340"
x:Class="LanMountainDesktop.Views.Components.ClassScheduleSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="课表导入"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="导入 ClassIsland 的 CSES 课表文件并选择启用项。"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Button x:Name="AddScheduleButton"
Grid.Row="2"
HorizontalAlignment="Left"
MinWidth="132"
Padding="12,8"
Click="OnAddScheduleClick">
<StackPanel Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<fi:FluentIcon Icon="Add" />
<TextBlock x:Name="AddScheduleButtonTextBlock"
Text="添加课表"
VerticalAlignment="Center" />
</StackPanel>
</Button>
<Grid Grid.Row="3">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="ScheduleItemsPanel"
Spacing="8" />
</ScrollViewer>
<TextBlock x:Name="EmptyStateTextBlock"
IsVisible="False"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="暂无导入课表" />
</Grid>
</Grid>
</Border>
</UserControl>

View File

@@ -1,369 +0,0 @@
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.Platform.Storage;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class ClassScheduleSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private readonly AppSettingsService _appSettingsService = 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;
public ClassScheduleSettingsWindow()
{
InitializeComponent();
LoadState();
ApplyLocalization();
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 = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
_importedSchedules.Clear();
foreach (var item in componentSnapshot.ImportedClassSchedules)
{
if (string.IsNullOrWhiteSpace(item.Id) ||
string.IsNullOrWhiteSpace(item.FilePath))
{
continue;
}
_importedSchedules.Add(new ImportedClassScheduleSnapshot
{
Id = item.Id.Trim(),
DisplayName = item.DisplayName?.Trim() ?? string.Empty,
FilePath = item.FilePath.Trim()
});
}
_activeScheduleId = componentSnapshot.ActiveImportedClassScheduleId?.Trim() ?? string.Empty;
if (_importedSchedules.Count > 0 &&
!_importedSchedules.Any(item => string.Equals(item.Id, _activeScheduleId, StringComparison.OrdinalIgnoreCase)))
{
_activeScheduleId = _importedSchedules[0].Id;
}
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("schedule.settings.title", "课表导入");
DescriptionTextBlock.Text = L(
"schedule.settings.desc",
"导入 ClassIsland 的 CSES 课表文件并选择启用项。");
AddScheduleButtonTextBlock.Text = L("schedule.settings.add", "添加课表");
EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表");
}
private async void OnAddScheduleClick(object? sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
var storageProvider = topLevel?.StorageProvider;
if (storageProvider is null)
{
return;
}
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = L("schedule.settings.picker_title", "选择 ClassIsland 课表文件"),
AllowMultiple = false,
FileTypeFilter =
[
new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES 课表"))
{
Patterns = ["*.cses", "*.yaml", "*.yml"]
}
]
});
if (files.Count == 0)
{
return;
}
var importedPath = await ImportScheduleFileAsync(files[0]);
if (string.IsNullOrWhiteSpace(importedPath))
{
return;
}
var existing = _importedSchedules.FirstOrDefault(item =>
string.Equals(item.FilePath, importedPath, StringComparison.OrdinalIgnoreCase));
if (existing is not null)
{
_activeScheduleId = existing.Id;
SaveState();
RenderImportedSchedules();
return;
}
var displayName = Path.GetFileNameWithoutExtension(importedPath)?.Trim();
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = L("schedule.settings.unnamed", "未命名课表");
}
var imported = new ImportedClassScheduleSnapshot
{
Id = Guid.NewGuid().ToString("N"),
DisplayName = displayName,
FilePath = importedPath
};
_importedSchedules.Add(imported);
_activeScheduleId = imported.Id;
SaveState();
RenderImportedSchedules();
}
private async Task<string?> ImportScheduleFileAsync(IStorageFile file)
{
try
{
var extension = Path.GetExtension(file.Name);
if (string.IsNullOrWhiteSpace(extension))
{
extension = ".cses";
}
var importedDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop",
"Schedules");
Directory.CreateDirectory(importedDirectory);
var destinationPath = Path.Combine(
importedDirectory,
$"{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 RenderImportedSchedules()
{
ScheduleItemsPanel.Children.Clear();
if (_importedSchedules.Count == 0)
{
EmptyStateTextBlock.IsVisible = true;
return;
}
EmptyStateTextBlock.IsVisible = false;
foreach (var item in _importedSchedules)
{
var selector = new RadioButton
{
GroupName = "class_schedule_imports",
IsChecked = string.Equals(item.Id, _activeScheduleId, StringComparison.OrdinalIgnoreCase),
VerticalAlignment = VerticalAlignment.Center,
Tag = item.Id
};
selector.IsCheckedChanged += OnScheduleSelectionChanged;
var title = new TextBlock
{
Text = string.IsNullOrWhiteSpace(item.DisplayName)
? L("schedule.settings.unnamed", "未命名课表")
: item.DisplayName,
FontSize = 14,
FontWeight = FontWeight.SemiBold,
Foreground = ResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FFEFF3FF"),
TextTrimming = TextTrimming.CharacterEllipsis
};
var path = new TextBlock
{
Text = item.FilePath,
FontSize = 11,
Foreground = ResolveThemeBrush("AdaptiveTextSecondaryBrush", "#FF99A2B5"),
TextTrimming = TextTrimming.CharacterEllipsis,
TextWrapping = TextWrapping.NoWrap
};
var textStack = new StackPanel
{
Spacing = 4,
VerticalAlignment = VerticalAlignment.Center,
Children = { title, path }
};
var deleteButton = new Button
{
Content = L("schedule.settings.delete", "删除"),
Tag = item.Id,
Padding = new Thickness(10, 6),
MinWidth = 64,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
deleteButton.Click += OnDeleteScheduleClick;
var rowGrid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
ColumnSpacing = 10
};
rowGrid.Children.Add(selector);
rowGrid.Children.Add(textStack);
rowGrid.Children.Add(deleteButton);
Grid.SetColumn(selector, 0);
Grid.SetColumn(textStack, 1);
Grid.SetColumn(deleteButton, 2);
var rowBorder = new Border
{
Padding = new Thickness(10, 8),
CornerRadius = new CornerRadius(12),
Background = ResolveThemeBrush("AdaptiveSurfaceRaisedBrush", "#1AFFFFFF"),
BorderBrush = ResolveThemeBrush("AdaptiveButtonBorderBrush", "#22000000"),
BorderThickness = new Thickness(1),
Child = rowGrid
};
ScheduleItemsPanel.Children.Add(rowBorder);
}
}
private void OnScheduleSelectionChanged(object? sender, RoutedEventArgs e)
{
if (sender is not RadioButton button ||
button.IsChecked != true ||
button.Tag is not string scheduleId)
{
return;
}
if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase))
{
return;
}
_activeScheduleId = scheduleId;
SaveState();
}
private void OnDeleteScheduleClick(object? sender, RoutedEventArgs e)
{
if (sender is not Button button || button.Tag is not string scheduleId)
{
return;
}
var target = _importedSchedules.FirstOrDefault(item =>
string.Equals(item.Id, scheduleId, StringComparison.OrdinalIgnoreCase));
if (target is null)
{
return;
}
_importedSchedules.Remove(target);
TryDeleteImportedFile(target.FilePath);
if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase))
{
_activeScheduleId = _importedSchedules.Count > 0 ? _importedSchedules[0].Id : string.Empty;
}
SaveState();
RenderImportedSchedules();
}
private void SaveState()
{
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.ImportedClassSchedules = _importedSchedules
.Select(item => new ImportedClassScheduleSnapshot
{
Id = item.Id,
DisplayName = item.DisplayName,
FilePath = item.FilePath
})
.ToList();
snapshot.ActiveImportedClassScheduleId = _activeScheduleId ?? string.Empty;
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
private static void TryDeleteImportedFile(string? filePath)
{
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
return;
}
try
{
File.Delete(filePath);
}
catch
{
// Keep settings operation resilient even when file deletion fails.
}
}
private IBrush ResolveThemeBrush(string key, string fallbackHex)
{
if (this.TryFindResource(key, out var value) && value is IBrush brush)
{
return brush;
}
return new SolidColorBrush(Color.Parse(fallbackHex));
}
}

View File

@@ -1,87 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.CnrDailyNewsSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="CNR news settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure auto-rotation and refresh interval."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRotateLabelTextBlock"
Text="Auto-rotation"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRotateCheckBox"
Content="Enable auto-rotation"
Checked="OnAutoRotateChanged"
Unchecked="OnAutoRotateChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Rotation interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency5mItem"
Tag="5"
Content="5 min" />
<ComboBoxItem x:Name="Frequency10mItem"
Tag="10"
Content="10 min" />
<ComboBoxItem x:Name="Frequency40mItem"
Tag="40"
Content="40 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
<ComboBoxItem x:Name="Frequency12hItem"
Tag="720"
Content="12 hours" />
<ComboBoxItem x:Name="Frequency24hItem"
Tag="1440"
Content="24 hours" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,153 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class CnrDailyNewsSettingsWindow : UserControl
{
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private bool _suppressEvents;
private string _languageCode = "zh-CN";
public event EventHandler? SettingsChanged;
public CnrDailyNewsSettingsWindow()
{
InitializeComponent();
InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var enabled = componentSnapshot.CnrDailyNewsAutoRotateEnabled;
var interval = NormalizeInterval(componentSnapshot.CnrDailyNewsAutoRotateIntervalMinutes);
_suppressEvents = true;
AutoRotateCheckBox.IsChecked = enabled;
SelectInterval(interval);
FrequencyCardBorder.IsVisible = enabled;
_suppressEvents = false;
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("cnrnews.settings.title", "CNR news settings");
DescriptionTextBlock.Text = L("cnrnews.settings.desc", "Configure auto-rotation and refresh interval.");
AutoRotateLabelTextBlock.Text = L("cnrnews.settings.auto_rotate_label", "Auto-rotation");
AutoRotateCheckBox.Content = L("cnrnews.settings.auto_rotate_enabled", "Enable auto-rotation");
FrequencyLabelTextBlock.Text = L("cnrnews.settings.frequency_label", "Rotation interval");
ApplyFrequencyLocalization();
}
private void OnAutoRotateChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var enabled = AutoRotateCheckBox.IsChecked == true;
FrequencyCardBorder.IsVisible = enabled;
SaveState();
}
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var snapshot = _componentSettingsService.Load();
snapshot.CnrDailyNewsAutoRotateEnabled = AutoRotateCheckBox.IsChecked == true;
snapshot.CnrDailyNewsAutoRotateIntervalMinutes = GetSelectedInterval();
_componentSettingsService.Save(snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private int GetSelectedInterval()
{
if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes))
{
return NormalizeInterval(minutes);
}
return 60;
}
private void SelectInterval(int intervalMinutes)
{
var selected = FrequencyComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private static int NormalizeInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 60);
}
private void InitializeFrequencyOptions()
{
FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
}
private void ApplyFrequencyLocalization()
{
foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
if (item.Tag is not string tagText ||
!int.TryParse(tagText, out var minutes))
{
continue;
}
var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
}
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,54 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="280"
x:Class="LanMountainDesktop.Views.Components.DailyArtworkSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<StackPanel Spacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="每日图片设置"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Text="切换每日图片的数据源。"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="10">
<StackPanel Spacing="8">
<TextBlock x:Name="MirrorSourceLabelTextBlock"
Text="镜像源"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="MirrorSourceComboBox"
Width="240"
SelectionChanged="OnMirrorSourceSelectionChanged">
<ComboBoxItem x:Name="MirrorSourceDomesticItem"
Tag="Domestic"
Content="国内镜像" />
<ComboBoxItem x:Name="MirrorSourceOverseasItem"
Tag="Overseas"
Content="国外镜像" />
</ComboBox>
</StackPanel>
</Border>
<TextBlock x:Name="StatusTextBlock"
Text="当前源:国内镜像"
FontSize="11"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
</StackPanel>
</Border>
</UserControl>

View File

@@ -1,117 +0,0 @@
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, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private readonly AppSettingsService _appSettingsService = 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;
public DailyArtworkSettingsWindow()
{
InitializeComponent();
LoadState();
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 = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var source = DailyArtworkMirrorSources.Normalize(componentSnapshot.DailyArtworkMirrorSource);
_suppressEvents = true;
MirrorSourceComboBox.SelectedIndex = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
? 0
: 1;
_suppressEvents = false;
UpdateSourceStatus(source);
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("artwork.settings.title", "每日图片设置");
DescriptionTextBlock.Text = L("artwork.settings.desc", "切换每日图片的数据源。");
MirrorSourceLabelTextBlock.Text = L("artwork.settings.source_label", "镜像源");
MirrorSourceDomesticItem.Content = L("artwork.settings.source_domestic", "国内镜像");
MirrorSourceOverseasItem.Content = L("artwork.settings.source_overseas", "国外镜像");
UpdateSourceStatus(GetSelectedSource());
}
private void OnMirrorSourceSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var source = GetSelectedSource();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.DailyArtworkMirrorSource = source;
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
UpdateSourceStatus(source);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string GetSelectedSource()
{
if (MirrorSourceComboBox.SelectedItem is ComboBoxItem comboBoxItem &&
comboBoxItem.Tag is string tagValue)
{
return DailyArtworkMirrorSources.Normalize(tagValue);
}
return DailyArtworkMirrorSources.Overseas;
}
private void UpdateSourceStatus(string source)
{
if (StatusTextBlock is null)
{
return;
}
StatusTextBlock.Text = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
? L("artwork.settings.source_status_domestic", "当前源:国内镜像(优先中国网络)")
: L("artwork.settings.source_status_overseas", "当前源:国外镜像(艺术馆推荐)");
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,87 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.DailyWordSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="Daily word settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure auto refresh and refresh interval."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRefreshLabelTextBlock"
Text="Auto refresh"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRefreshCheckBox"
Content="Enable auto refresh"
Checked="OnAutoRefreshChanged"
Unchecked="OnAutoRefreshChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Refresh interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency30mItem"
Tag="30"
Content="30 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
<ComboBoxItem x:Name="Frequency3hItem"
Tag="180"
Content="3 hours" />
<ComboBoxItem x:Name="Frequency6hItem"
Tag="360"
Content="6 hours" />
<ComboBoxItem x:Name="Frequency12hItem"
Tag="720"
Content="12 hours" />
<ComboBoxItem x:Name="Frequency24hItem"
Tag="1440"
Content="24 hours" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,153 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class DailyWordSettingsWindow : UserControl
{
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private bool _suppressEvents;
private string _languageCode = "zh-CN";
public event EventHandler? SettingsChanged;
public DailyWordSettingsWindow()
{
InitializeComponent();
InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var enabled = componentSnapshot.DailyWordAutoRefreshEnabled;
var interval = NormalizeInterval(componentSnapshot.DailyWordAutoRefreshIntervalMinutes);
_suppressEvents = true;
AutoRefreshCheckBox.IsChecked = enabled;
SelectInterval(interval);
FrequencyCardBorder.IsVisible = enabled;
_suppressEvents = false;
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("dailyword.settings.title", "Daily word settings");
DescriptionTextBlock.Text = L("dailyword.settings.desc", "Configure auto refresh and refresh interval.");
AutoRefreshLabelTextBlock.Text = L("dailyword.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("dailyword.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("dailyword.settings.frequency_label", "Refresh interval");
ApplyFrequencyLocalization();
}
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var enabled = AutoRefreshCheckBox.IsChecked == true;
FrequencyCardBorder.IsVisible = enabled;
SaveState();
}
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var snapshot = _componentSettingsService.Load();
snapshot.DailyWordAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
snapshot.DailyWordAutoRefreshIntervalMinutes = GetSelectedInterval();
_componentSettingsService.Save(snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private int GetSelectedInterval()
{
if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes))
{
return NormalizeInterval(minutes);
}
return 360;
}
private void SelectInterval(int intervalMinutes)
{
var selected = FrequencyComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private static int NormalizeInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 360);
}
private void InitializeFrequencyOptions()
{
FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
}
private void ApplyFrequencyLocalization()
{
foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
if (item.Tag is not string tagText ||
!int.TryParse(tagText, out var minutes))
{
continue;
}
var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
}
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,20 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.DateWidgetSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="Date Widget Settings"
FontSize="20"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</StackPanel>
</Border>
</UserControl>

View File

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

View File

@@ -1,113 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="320"
x:Class="LanMountainDesktop.Views.Components.IfengNewsSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="iFeng news settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure channel, auto refresh and refresh interval."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="ChannelLabelTextBlock"
Text="News channel"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="ChannelComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnChannelSelectionChanged">
<ComboBoxItem x:Name="ChannelComprehensiveItem"
Tag="Comprehensive"
Content="Comprehensive" />
<ComboBoxItem x:Name="ChannelMainlandItem"
Tag="Mainland"
Content="China Mainland" />
<ComboBoxItem x:Name="ChannelTaiwanItem"
Tag="Taiwan"
Content="Taiwan" />
</ComboBox>
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRefreshLabelTextBlock"
Text="Auto refresh"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRefreshCheckBox"
Content="Enable auto refresh"
Checked="OnAutoRefreshChanged"
Unchecked="OnAutoRefreshChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Refresh interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency5mItem"
Tag="5"
Content="5 min" />
<ComboBoxItem x:Name="Frequency10mItem"
Tag="10"
Content="10 min" />
<ComboBoxItem x:Name="Frequency15mItem"
Tag="15"
Content="15 min" />
<ComboBoxItem x:Name="Frequency20mItem"
Tag="20"
Content="20 min" />
<ComboBoxItem x:Name="Frequency30mItem"
Tag="30"
Content="30 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,194 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class IfengNewsSettingsWindow : UserControl
{
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private bool _suppressEvents;
private string _languageCode = "zh-CN";
public event EventHandler? SettingsChanged;
public IfengNewsSettingsWindow()
{
InitializeComponent();
InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var channelType = IfengNewsChannelTypes.Normalize(componentSnapshot.IfengNewsChannelType);
var enabled = componentSnapshot.IfengNewsAutoRefreshEnabled;
var interval = NormalizeInterval(componentSnapshot.IfengNewsAutoRefreshIntervalMinutes);
_suppressEvents = true;
SelectChannelType(channelType);
AutoRefreshCheckBox.IsChecked = enabled;
SelectInterval(interval);
FrequencyCardBorder.IsVisible = enabled;
_suppressEvents = false;
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("ifeng.settings.title", "iFeng news settings");
DescriptionTextBlock.Text = L("ifeng.settings.desc", "Configure channel, auto refresh and refresh interval.");
ChannelLabelTextBlock.Text = L("ifeng.settings.channel_label", "News channel");
ChannelComprehensiveItem.Content = L("ifeng.settings.channel_comprehensive", "Comprehensive");
ChannelMainlandItem.Content = L("ifeng.settings.channel_mainland", "China Mainland");
ChannelTaiwanItem.Content = L("ifeng.settings.channel_taiwan", "Taiwan");
AutoRefreshLabelTextBlock.Text = L("ifeng.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("ifeng.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("ifeng.settings.frequency_label", "Refresh interval");
ApplyFrequencyLocalization();
}
private void OnChannelSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var enabled = AutoRefreshCheckBox.IsChecked == true;
FrequencyCardBorder.IsVisible = enabled;
SaveState();
}
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var snapshot = _componentSettingsService.Load();
snapshot.IfengNewsChannelType = GetSelectedChannelType();
snapshot.IfengNewsAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
snapshot.IfengNewsAutoRefreshIntervalMinutes = GetSelectedInterval();
_componentSettingsService.Save(snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string GetSelectedChannelType()
{
if (ChannelComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string channelTag)
{
return IfengNewsChannelTypes.Normalize(channelTag);
}
return IfengNewsChannelTypes.Comprehensive;
}
private int GetSelectedInterval()
{
if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes))
{
return NormalizeInterval(minutes);
}
return 20;
}
private void SelectChannelType(string channelType)
{
var normalizedChannelType = IfengNewsChannelTypes.Normalize(channelType);
var selected = ChannelComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string channelTag &&
string.Equals(IfengNewsChannelTypes.Normalize(channelTag), normalizedChannelType, StringComparison.OrdinalIgnoreCase));
ChannelComboBox.SelectedItem = selected ?? ChannelComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private void SelectInterval(int intervalMinutes)
{
var selected = FrequencyComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private static int NormalizeInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 20);
}
private void InitializeFrequencyOptions()
{
FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
}
private void ApplyFrequencyLocalization()
{
foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
if (item.Tag is not string tagText ||
!int.TryParse(tagText, out var minutes))
{
continue;
}
var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
}
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,128 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.Stcn24ForumSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="STCN 24 settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure information source, auto refresh and refresh interval."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="SourceLabelTextBlock"
Text="Information source"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="SourceComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnSourceSelectionChanged">
<ComboBoxItem x:Name="SourceLatestCreatedItem"
Tag="LatestCreated"
Content="Latest posts" />
<ComboBoxItem x:Name="SourceLatestActivityItem"
Tag="LatestActivity"
Content="Latest activity" />
<ComboBoxItem x:Name="SourceMostRepliesItem"
Tag="MostReplies"
Content="Most replies" />
<ComboBoxItem x:Name="SourceEarliestCreatedItem"
Tag="EarliestCreated"
Content="Earliest posts" />
<ComboBoxItem x:Name="SourceEarliestActivityItem"
Tag="EarliestActivity"
Content="Earliest activity" />
<ComboBoxItem x:Name="SourceLeastRepliesItem"
Tag="LeastReplies"
Content="Least replies" />
<ComboBoxItem x:Name="SourceFrontpageLatestItem"
Tag="FrontpageLatest"
Content="Frontpage latest" />
<ComboBoxItem x:Name="SourceFrontpageEarliestItem"
Tag="FrontpageEarliest"
Content="Frontpage earliest" />
</ComboBox>
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRefreshLabelTextBlock"
Text="Auto refresh"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRefreshCheckBox"
Content="Enable auto refresh"
Checked="OnAutoRefreshChanged"
Unchecked="OnAutoRefreshChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Refresh interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency5mItem"
Tag="5"
Content="5 min" />
<ComboBoxItem x:Name="Frequency10mItem"
Tag="10"
Content="10 min" />
<ComboBoxItem x:Name="Frequency20mItem"
Tag="20"
Content="20 min" />
<ComboBoxItem x:Name="Frequency30mItem"
Tag="30"
Content="30 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
<ComboBoxItem x:Name="Frequency3hItem"
Tag="180"
Content="3 hours" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,199 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class Stcn24ForumSettingsWindow : UserControl
{
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private bool _suppressEvents;
private string _languageCode = "zh-CN";
public event EventHandler? SettingsChanged;
public Stcn24ForumSettingsWindow()
{
InitializeComponent();
InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var enabled = componentSnapshot.Stcn24ForumAutoRefreshEnabled;
var interval = NormalizeInterval(componentSnapshot.Stcn24ForumAutoRefreshIntervalMinutes);
var sourceType = Stcn24ForumSourceTypes.Normalize(componentSnapshot.Stcn24ForumSourceType);
_suppressEvents = true;
AutoRefreshCheckBox.IsChecked = enabled;
SelectSourceType(sourceType);
SelectInterval(interval);
FrequencyCardBorder.IsVisible = enabled;
_suppressEvents = false;
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("stcn24.settings.title", "STCN 24 settings");
DescriptionTextBlock.Text = L("stcn24.settings.desc", "Configure information source, auto refresh and refresh interval.");
SourceLabelTextBlock.Text = L("stcn24.settings.source_label", "Information source");
SourceLatestCreatedItem.Content = L("stcn24.settings.source_latest_created", "Latest posts");
SourceLatestActivityItem.Content = L("stcn24.settings.source_latest_activity", "Latest activity");
SourceMostRepliesItem.Content = L("stcn24.settings.source_most_replies", "Most replies");
SourceEarliestCreatedItem.Content = L("stcn24.settings.source_earliest_created", "Earliest posts");
SourceEarliestActivityItem.Content = L("stcn24.settings.source_earliest_activity", "Earliest activity");
SourceLeastRepliesItem.Content = L("stcn24.settings.source_least_replies", "Least replies");
SourceFrontpageLatestItem.Content = L("stcn24.settings.source_frontpage_latest", "Frontpage latest");
SourceFrontpageEarliestItem.Content = L("stcn24.settings.source_frontpage_earliest", "Frontpage earliest");
AutoRefreshLabelTextBlock.Text = L("stcn24.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("stcn24.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("stcn24.settings.frequency_label", "Refresh interval");
ApplyFrequencyLocalization();
}
private void OnSourceSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var enabled = AutoRefreshCheckBox.IsChecked == true;
FrequencyCardBorder.IsVisible = enabled;
SaveState();
}
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var snapshot = _componentSettingsService.Load();
snapshot.Stcn24ForumSourceType = GetSelectedSourceType();
snapshot.Stcn24ForumAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
snapshot.Stcn24ForumAutoRefreshIntervalMinutes = GetSelectedInterval();
_componentSettingsService.Save(snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string GetSelectedSourceType()
{
if (SourceComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string sourceTag)
{
return Stcn24ForumSourceTypes.Normalize(sourceTag);
}
return Stcn24ForumSourceTypes.LatestCreated;
}
private int GetSelectedInterval()
{
if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes))
{
return NormalizeInterval(minutes);
}
return 20;
}
private void SelectInterval(int intervalMinutes)
{
var selected = FrequencyComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private void SelectSourceType(string sourceType)
{
var normalizedSourceType = Stcn24ForumSourceTypes.Normalize(sourceType);
var selected = SourceComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string sourceTag &&
string.Equals(Stcn24ForumSourceTypes.Normalize(sourceTag), normalizedSourceType, StringComparison.OrdinalIgnoreCase));
SourceComboBox.SelectedItem = selected ?? SourceComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private static int NormalizeInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 20);
}
private void InitializeFrequencyOptions()
{
FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
}
private void ApplyFrequencyLocalization()
{
foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
if (item.Tag is not string tagText ||
!int.TryParse(tagText, out var minutes))
{
continue;
}
var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
}
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,50 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="280"
x:Class="LanMountainDesktop.Views.Components.StudyEnvironmentWidgetSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<StackPanel Spacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="环境组件设置"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Text="配置右侧噪音值展示。"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="10">
<StackPanel Spacing="8">
<CheckBox x:Name="ShowDisplayDbCheckBox"
IsChecked="True"
Content="显示 display dB"
Checked="OnDisplayModeChanged"
Unchecked="OnDisplayModeChanged" />
<CheckBox x:Name="ShowDbfsCheckBox"
IsChecked="False"
Content="显示 dBFS"
Checked="OnDisplayModeChanged"
Unchecked="OnDisplayModeChanged" />
</StackPanel>
</Border>
<TextBlock x:Name="HintTextBlock"
Text="至少启用一种显示方式。"
FontSize="11"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
</StackPanel>
</Border>
</UserControl>

View File

@@ -1,92 +0,0 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class StudyEnvironmentWidgetSettingsWindow : UserControl
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private string _languageCode = "zh-CN";
private bool _suppressEvents;
public event EventHandler? SettingsChanged;
public StudyEnvironmentWidgetSettingsWindow()
{
InitializeComponent();
LoadState();
ApplyLocalization();
}
private void LoadState()
{
var appSnapshot = _appSettingsService.Load();
var componentSnapshot = _componentSettingsService.Load();
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var showDisplayDb = componentSnapshot.StudyEnvironmentShowDisplayDb;
var showDbfs = componentSnapshot.StudyEnvironmentShowDbfs;
if (!showDisplayDb && !showDbfs)
{
showDisplayDb = true;
}
_suppressEvents = true;
ShowDisplayDbCheckBox.IsChecked = showDisplayDb;
ShowDbfsCheckBox.IsChecked = showDbfs;
_suppressEvents = false;
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("study.environment.settings.title", "环境组件设置");
DescriptionTextBlock.Text = L(
"study.environment.settings.desc",
"配置右侧实时噪音值显示内容。");
ShowDisplayDbCheckBox.Content = L(
"study.environment.settings.show_display_db",
"显示 display dB");
ShowDbfsCheckBox.Content = L(
"study.environment.settings.show_dbfs",
"显示 dBFS");
HintTextBlock.Text = L(
"study.environment.settings.hint",
"至少启用一种显示方式。");
}
private void OnDisplayModeChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var showDisplayDb = ShowDisplayDbCheckBox.IsChecked == true;
var showDbfs = ShowDbfsCheckBox.IsChecked == true;
if (!showDisplayDb && !showDbfs)
{
_suppressEvents = true;
ShowDisplayDbCheckBox.IsChecked = true;
_suppressEvents = false;
showDisplayDb = true;
}
var snapshot = _componentSettingsService.Load();
snapshot.StudyEnvironmentShowDisplayDb = showDisplayDb;
snapshot.StudyEnvironmentShowDbfs = showDbfs;
_componentSettingsService.Save(snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,87 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.WeatherWidgetSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="Weather widget settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure auto refresh and refresh interval for all weather widgets."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRefreshLabelTextBlock"
Text="Auto refresh"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRefreshCheckBox"
Content="Enable auto refresh"
Checked="OnAutoRefreshChanged"
Unchecked="OnAutoRefreshChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Refresh interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency10mItem"
Tag="10"
Content="10 min" />
<ComboBoxItem x:Name="Frequency12mItem"
Tag="12"
Content="12 min" />
<ComboBoxItem x:Name="Frequency15mItem"
Tag="15"
Content="15 min" />
<ComboBoxItem x:Name="Frequency30mItem"
Tag="30"
Content="30 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
<ComboBoxItem x:Name="Frequency3hItem"
Tag="180"
Content="3 hours" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,173 +0,0 @@
using System;
using System.Collections.Generic;
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, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = 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;
public WeatherWidgetSettingsWindow()
{
InitializeComponent();
InitializeFrequencyOptions();
LoadState();
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 = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
var enabled = componentSnapshot.WeatherAutoRefreshEnabled;
var interval = NormalizeInterval(componentSnapshot.WeatherAutoRefreshIntervalMinutes);
_suppressEvents = true;
AutoRefreshCheckBox.IsChecked = enabled;
SelectInterval(interval);
FrequencyCardBorder.IsVisible = enabled;
_suppressEvents = false;
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("weather.widget.settings.title", "Weather widget settings");
DescriptionTextBlock.Text = L("weather.widget.settings.desc", "Configure auto refresh and refresh interval for all weather widgets.");
AutoRefreshLabelTextBlock.Text = L("weather.widget.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("weather.widget.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("weather.widget.settings.frequency_label", "Refresh interval");
ApplyFrequencyLocalization();
}
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
var enabled = AutoRefreshCheckBox.IsChecked == true;
FrequencyCardBorder.IsVisible = enabled;
SaveState();
}
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.WeatherAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
snapshot.WeatherAutoRefreshIntervalMinutes = GetSelectedInterval();
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private int GetSelectedInterval()
{
if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes))
{
return NormalizeInterval(minutes);
}
return 12;
}
private void SelectInterval(int intervalMinutes)
{
var selected = FrequencyComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private static int NormalizeInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 12);
}
private void InitializeFrequencyOptions()
{
FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
}
private void ApplyFrequencyLocalization()
{
foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
if (item.Tag is not string tagText ||
!int.TryParse(tagText, out var minutes))
{
continue;
}
var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
}
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,121 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="560"
d:DesignHeight="380"
x:Class="LanMountainDesktop.Views.Components.WorldClockWidgetSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="世界时钟设置"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="分别为四个时钟选择时区。"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="SecondHandModeLabelTextBlock"
Text="秒针方式"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<StackPanel Orientation="Horizontal"
Spacing="12">
<RadioButton x:Name="SecondHandTickRadioButton"
GroupName="world_clock_second_mode"
Content="跳针"
Checked="OnSecondHandModeChanged" />
<RadioButton x:Name="SecondHandSweepRadioButton"
GroupName="world_clock_second_mode"
Content="扫针"
Checked="OnSecondHandModeChanged" />
</StackPanel>
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="ClockOneLabelTextBlock"
Text="时钟 1"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="ClockOneTimeZoneComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnTimeZoneSelectionChanged" />
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="ClockTwoLabelTextBlock"
Text="时钟 2"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="ClockTwoTimeZoneComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnTimeZoneSelectionChanged" />
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="ClockThreeLabelTextBlock"
Text="时钟 3"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="ClockThreeTimeZoneComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnTimeZoneSelectionChanged" />
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="ClockFourLabelTextBlock"
Text="时钟 4"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="ClockFourTimeZoneComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnTimeZoneSelectionChanged" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -1,268 +0,0 @@
using System;
using System.Collections.Generic;
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, IComponentPlacementContextAware, IComponentSettingsStoreAware
{
private static readonly IReadOnlyDictionary<string, string> ZhTimeZoneNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["China Standard Time"] = "中国标准时间",
["Asia/Shanghai"] = "中国标准时间",
["GMT Standard Time"] = "格林威治标准时间",
["Europe/London"] = "格林威治标准时间",
["AUS Eastern Standard Time"] = "澳大利亚东部标准时间",
["Australia/Sydney"] = "澳大利亚东部标准时间",
["Eastern Standard Time"] = "美国东部标准时间",
["America/New_York"] = "美国东部标准时间",
["Tokyo Standard Time"] = "日本标准时间",
["Asia/Tokyo"] = "日本标准时间",
["UTC"] = "协调世界时",
["Etc/UTC"] = "协调世界时"
};
private readonly AppSettingsService _appSettingsService = 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;
public event EventHandler? SettingsChanged;
public WorldClockWidgetSettingsWindow()
{
InitializeComponent();
_timeZoneComboBoxes =
[
ClockOneTimeZoneComboBox,
ClockTwoTimeZoneComboBox,
ClockThreeTimeZoneComboBox,
ClockFourTimeZoneComboBox
];
LoadState();
ApplyLocalization();
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 = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
_allTimeZones = _timeZoneService
.GetAllTimeZones()
.OrderBy(zone => zone.GetUtcOffset(DateTime.UtcNow))
.ThenBy(zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
_selectedTimeZoneIds = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(
componentSnapshot.WorldClockTimeZoneIds,
_allTimeZones);
_secondHandMode = ClockSecondHandMode.Normalize(componentSnapshot.WorldClockSecondHandMode);
}
private void ApplyLocalization()
{
TitleTextBlock.Text = L("worldclock.settings.title", "世界时钟设置");
DescriptionTextBlock.Text = L("worldclock.settings.desc", "分别为四个时钟选择时区。");
ClockOneLabelTextBlock.Text = L("worldclock.settings.clock_1", "时钟 1");
ClockTwoLabelTextBlock.Text = L("worldclock.settings.clock_2", "时钟 2");
ClockThreeLabelTextBlock.Text = L("worldclock.settings.clock_3", "时钟 3");
ClockFourLabelTextBlock.Text = L("worldclock.settings.clock_4", "时钟 4");
SecondHandModeLabelTextBlock.Text = L("worldclock.settings.second_mode_label", "秒针方式");
SecondHandTickRadioButton.Content = L("clock.second_mode.tick", "跳针");
SecondHandSweepRadioButton.Content = L("clock.second_mode.sweep", "扫针");
}
private void PopulateTimeZoneComboBoxes()
{
_suppressEvents = true;
try
{
foreach (var comboBox in _timeZoneComboBoxes)
{
comboBox.Items.Clear();
foreach (var timeZone in _allTimeZones)
{
comboBox.Items.Add(new ComboBoxItem
{
Tag = timeZone.Id,
Content = GetLocalizedTimeZoneDisplayName(timeZone)
});
}
}
for (var index = 0; index < _timeZoneComboBoxes.Length; index++)
{
var comboBox = _timeZoneComboBoxes[index];
var targetId = index < _selectedTimeZoneIds.Count
? _selectedTimeZoneIds[index]
: TimeZoneInfo.Local.Id;
var selected = comboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item => string.Equals(item.Tag as string, targetId, StringComparison.OrdinalIgnoreCase));
comboBox.SelectedItem = selected ?? comboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
var normalizedMode = ClockSecondHandMode.Normalize(_secondHandMode);
SecondHandTickRadioButton.IsChecked = string.Equals(
normalizedMode,
ClockSecondHandMode.Tick,
StringComparison.OrdinalIgnoreCase);
SecondHandSweepRadioButton.IsChecked = string.Equals(
normalizedMode,
ClockSecondHandMode.Sweep,
StringComparison.OrdinalIgnoreCase);
}
finally
{
_suppressEvents = false;
}
}
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void OnSecondHandModeChanged(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_suppressEvents)
{
return;
}
SaveState();
}
private void SaveState()
{
var selectedIds = GetSelectedTimeZoneIds();
var normalizedIds = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(selectedIds, _allTimeZones);
_secondHandMode = GetSelectedSecondHandMode();
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
snapshot.WorldClockTimeZoneIds = normalizedIds.ToList();
snapshot.WorldClockSecondHandMode = _secondHandMode;
_componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot);
_selectedTimeZoneIds = normalizedIds;
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private string GetSelectedSecondHandMode()
{
return SecondHandSweepRadioButton.IsChecked == true
? ClockSecondHandMode.Sweep
: ClockSecondHandMode.Tick;
}
private List<string> GetSelectedTimeZoneIds()
{
var selectedIds = new List<string>(_timeZoneComboBoxes.Length);
foreach (var comboBox in _timeZoneComboBoxes)
{
if (comboBox.SelectedItem is ComboBoxItem item &&
item.Tag is string timeZoneId &&
!string.IsNullOrWhiteSpace(timeZoneId))
{
selectedIds.Add(timeZoneId.Trim());
continue;
}
selectedIds.Add(TimeZoneInfo.Local.Id);
}
return selectedIds;
}
private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone)
{
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
var sign = offset >= TimeSpan.Zero ? "+" : "-";
var totalMinutes = Math.Abs((int)offset.TotalMinutes);
var hours = totalMinutes / 60;
var minutes = totalMinutes % 60;
var displayName = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase)
? ResolveZhDisplayName(timeZone)
: ResolveEnDisplayName(timeZone);
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {displayName}";
}
private static string ResolveZhDisplayName(TimeZoneInfo timeZone)
{
if (ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName))
{
return localizedName;
}
return string.IsNullOrWhiteSpace(timeZone.StandardName)
? timeZone.DisplayName
: timeZone.StandardName;
}
private static string ResolveEnDisplayName(TimeZoneInfo timeZone)
{
if (!string.IsNullOrWhiteSpace(timeZone.StandardName))
{
return timeZone.StandardName;
}
return timeZone.DisplayName;
}
private string L(string key, string fallback)
{
return _localizationService.GetString(_languageCode, key, fallback);
}
}

View File

@@ -1,23 +0,0 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using FluentAvalonia.UI.Windowing;
namespace LanMountainDesktop.Views;
public class IndependentSettingsModuleWindowBase : AppWindow
{
public IndependentSettingsModuleWindowBase()
{
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
TitleBar.Height = 48;
if (OperatingSystem.IsWindows())
{
TransparencyLevelHint = [WindowTransparencyLevel.Mica];
Background = Brushes.Transparent;
}
}
}

View File

@@ -1,9 +0,0 @@
namespace LanMountainDesktop.Views;
internal enum IndependentSettingsPageCategory
{
Internal = 0,
External = 1,
About = 2,
Debug = 3
}

View File

@@ -1,12 +0,0 @@
using FluentIcons.Common;
namespace LanMountainDesktop.Views;
internal sealed record IndependentSettingsPageDefinition(
string Tag,
string Title,
string Description,
Symbol Icon,
IndependentSettingsPageCategory Category,
int SortOrder,
string? ToolTip = null);

View File

@@ -108,23 +108,18 @@ public partial class MainWindow
return;
}
_reopenSettingsAfterComponentLibraryClose = _isSettingsOpen;
if (_isSettingsOpen)
{
CloseSettingsPage(immediate: true);
}
OpenComponentLibraryWindow();
}
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
{
CloseComponentLibraryWindow(reopenSettings: true);
CloseComponentLibraryWindow(reopenSettings: false);
}
private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e)
{
CloseComponentSettingsWindow();
_ = sender;
_ = e;
}
private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e)
@@ -251,29 +246,13 @@ public partial class MainWindow
private TaskbarContext GetCurrentTaskbarContext()
{
if (!_isSettingsOpen)
{
return TaskbarContext.Desktop;
}
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
};
return TaskbarContext.Desktop;
}
private void ApplyTaskbarActionVisibility(TaskbarContext context)
{
if (BackToWindowsButton is null ||
OpenComponentLibraryButton is null ||
OpenSettingsButton is null ||
WallpaperPreviewBackButtonVisual is null ||
WallpaperPreviewComponentLibraryVisual is null ||
WallpaperPreviewSettingsButtonIcon is null)
@@ -282,12 +261,11 @@ public partial class MainWindow
}
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings);
var showDesktopEdit = _isSettingsOpen;
var showSettings = false;
var showDesktopEdit = true;
BackToWindowsButton.IsVisible = showMinimize;
OpenComponentLibraryButton.IsVisible = showDesktopEdit;
OpenSettingsButton.IsVisible = showSettings;
WallpaperPreviewBackButtonVisual.IsVisible = showMinimize;
WallpaperPreviewComponentLibraryVisual.IsVisible = showDesktopEdit;
WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings;
@@ -327,30 +305,11 @@ public partial class MainWindow
{
WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
}
UpdateOpenSettingsActionVisualState();
}
private void UpdateOpenSettingsActionVisualState()
{
if (OpenSettingsButtonTextBlock is null || OpenSettingsButton is null)
{
return;
}
var showBackToDesktop = _isSettingsOpen;
OpenSettingsButtonTextBlock.IsVisible = showBackToDesktop;
OpenSettingsButtonTextBlock.Text = L("settings.back_to_desktop", "Back to Desktop");
ToolTip.SetTip(
OpenSettingsButton,
showBackToDesktop
? L("settings.back_to_desktop", "Back to Desktop")
: L("tooltip.open_settings", "Settings"));
var effectiveCellSize = _currentDesktopCellSize > 0
? _currentDesktopCellSize
: Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells));
ApplyWidgetSizing(effectiveCellSize);
// Open-settings action is removed in API-only settings mode.
}
private void OpenComponentLibraryWindow()
@@ -410,10 +369,9 @@ public partial class MainWindow
_reopenSettingsAfterComponentLibraryClose = false;
if (shouldReopenSettings)
{
if (Application.Current is App app)
{
app.OpenIndependentSettingsModule("ComponentLibrary");
}
AppLogger.Info(
"SettingsFacade",
"Reopen settings request ignored because settings UI entry is disabled during hard-cut migration.");
}
}, FluttermotionToken.Slow);
}
@@ -452,13 +410,6 @@ public partial class MainWindow
IsVisible: true,
CommandKey: "component.delete"));
actions.Add(new TaskbarActionItem(
TaskbarActionId.EditComponent,
L("component.edit", "Edit"),
"Edit",
IsVisible: true,
CommandKey: "component.edit"));
return actions;
}
@@ -556,17 +507,12 @@ public partial class MainWindow
var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage ||
action.Id == TaskbarActionId.DeleteComponent;
var isHideAction = action.Id == TaskbarActionId.HideLauncherEntry;
var isEditAction = action.Id == TaskbarActionId.EditComponent;
Symbol iconSymbol;
if (isDeleteAction || isHideAction)
{
iconSymbol = Symbol.Delete;
}
else if (isEditAction)
{
iconSymbol = Symbol.Edit;
}
else
{
iconSymbol = Symbol.Add;
@@ -662,9 +608,6 @@ public partial class MainWindow
case "component.delete":
DeleteSelectedComponent();
break;
case "component.edit":
OpenComponentSettings();
break;
case "launcher.hide":
HideSelectedLauncherEntry();
break;
@@ -714,583 +657,7 @@ public partial class MainWindow
}
}
private void OpenComponentSettings()
{
if (_selectedDesktopComponentHost is null || _selectedDesktopComponentHost.Tag is not string placementId)
{
return;
}
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
if (placement is null)
{
return;
}
if (placement.ComponentId == BuiltInComponentIds.Date)
{
OpenDateComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopClock)
{
OpenDesktopClockComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopClassSchedule)
{
OpenClassScheduleComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopWorldClock)
{
OpenWorldClockComponentSettings(placement);
return;
}
if (IsWeatherComponentId(placement.ComponentId))
{
OpenWeatherComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork)
{
OpenDailyArtworkComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopCnrDailyNews)
{
OpenCnrDailyNewsComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopIfengNews)
{
OpenIfengNewsComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyWord ||
placement.ComponentId == BuiltInComponentIds.DesktopDailyWord2x2)
{
OpenDailyWordComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopBilibiliHotSearch)
{
OpenBilibiliHotSearchComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopBaiduHotSearch)
{
OpenBaiduHotSearchComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopStcn24Forum)
{
OpenStcn24ForumComponentSettings(placement);
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment)
{
OpenStudyEnvironmentComponentSettings(placement);
return;
}
}
private static bool IsWeatherComponentId(string componentId)
{
return string.Equals(componentId, BuiltInComponentIds.DesktopWeather, StringComparison.OrdinalIgnoreCase) ||
string.Equals(componentId, BuiltInComponentIds.DesktopWeatherClock, StringComparison.OrdinalIgnoreCase) ||
string.Equals(componentId, BuiltInComponentIds.DesktopHourlyWeather, StringComparison.OrdinalIgnoreCase) ||
string.Equals(componentId, BuiltInComponentIds.DesktopMultiDayWeather, StringComparison.OrdinalIgnoreCase) ||
string.Equals(componentId, BuiltInComponentIds.DesktopExtendedWeather, StringComparison.OrdinalIgnoreCase);
}
private void ShowComponentSettings(Control settingsContent, DesktopComponentPlacementSnapshot placement)
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
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 OpenDateComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new DateWidgetSettingsWindow();
ShowComponentSettings(settingsContent, placement);
}
private void OpenClassScheduleComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new ClassScheduleSettingsWindow();
settingsContent.SettingsChanged += OnClassScheduleSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenDesktopClockComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new AnalogClockWidgetSettingsWindow();
settingsContent.SettingsChanged += OnDesktopClockSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenWorldClockComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new WorldClockWidgetSettingsWindow();
settingsContent.SettingsChanged += OnWorldClockSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenWeatherComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new WeatherWidgetSettingsWindow();
settingsContent.SettingsChanged += OnWeatherSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenStudyEnvironmentComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new StudyEnvironmentWidgetSettingsWindow();
settingsContent.SettingsChanged += OnStudyEnvironmentSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenDailyArtworkComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new DailyArtworkSettingsWindow();
settingsContent.SettingsChanged += OnDailyArtworkSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenCnrDailyNewsComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new CnrDailyNewsSettingsWindow();
settingsContent.SettingsChanged += OnCnrDailyNewsSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenIfengNewsComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new IfengNewsSettingsWindow();
settingsContent.SettingsChanged += OnIfengNewsSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenDailyWordComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new DailyWordSettingsWindow();
settingsContent.SettingsChanged += OnDailyWordSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenBilibiliHotSearchComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new BilibiliHotSearchSettingsWindow();
settingsContent.SettingsChanged += OnBilibiliHotSearchSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenBaiduHotSearchComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new BaiduHotSearchSettingsWindow();
settingsContent.SettingsChanged += OnBaiduHotSearchSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OpenStcn24ForumComponentSettings(DesktopComponentPlacementSnapshot placement)
{
var settingsContent = new Stcn24ForumSettingsWindow();
settingsContent.SettingsChanged += OnStcn24ForumSettingsChanged;
ShowComponentSettings(settingsContent, placement);
}
private void OnClassScheduleSettingsChanged(object? sender, EventArgs e)
{
if (_selectedDesktopComponentHost is null)
{
return;
}
if (TryGetContentHost(_selectedDesktopComponentHost)?.Child is ClassScheduleWidget widget)
{
widget.RefreshFromSettings();
}
}
private void OnDesktopClockSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is AnalogClockWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void OnStudyEnvironmentSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
if (_selectedDesktopComponentHost is null)
{
return;
}
if (TryGetContentHost(_selectedDesktopComponentHost)?.Child is StudyEnvironmentWidget widget)
{
widget.RefreshFromSettings();
}
}
private void OnDailyArtworkSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is DailyArtworkWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void OnWorldClockSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is WorldClockWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void OnWeatherSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
var child = TryGetContentHost(host)?.Child;
switch (child)
{
case WeatherWidget weatherWidget:
weatherWidget.RefreshFromSettings();
break;
case WeatherClockWidget weatherClockWidget:
weatherClockWidget.RefreshFromSettings();
break;
case HourlyWeatherWidget hourlyWeatherWidget:
hourlyWeatherWidget.RefreshFromSettings();
break;
case MultiDayWeatherWidget multiDayWeatherWidget:
multiDayWeatherWidget.RefreshFromSettings();
break;
case ExtendedWeatherWidget extendedWeatherWidget:
extendedWeatherWidget.RefreshFromSettings();
break;
}
}
}
}
private void OnCnrDailyNewsSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is CnrDailyNewsWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void OnIfengNewsSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is IfengNewsWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void OnDailyWordSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
var widget = TryGetContentHost(host)?.Child;
if (widget is DailyWordWidget dailyWordWidget)
{
dailyWordWidget.RefreshFromSettings();
}
else if (widget is DailyWord2x2Widget dailyWord2x2Widget)
{
dailyWord2x2Widget.RefreshFromSettings();
}
}
}
}
private void OnBilibiliHotSearchSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is BilibiliHotSearchWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void OnBaiduHotSearchSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is BaiduHotSearchWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void OnStcn24ForumSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
foreach (var pageGrid in _desktopPageComponentGrids.Values)
{
foreach (var host in pageGrid.Children.OfType<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is Stcn24ForumWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void CloseComponentSettingsWindow()
{
if (ComponentSettingsWindow is null)
{
return;
}
if (ComponentSettingsContentHost?.Content is ClassScheduleSettingsWindow classScheduleSettingsWindow)
{
classScheduleSettingsWindow.SettingsChanged -= OnClassScheduleSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is AnalogClockWidgetSettingsWindow analogClockSettingsWindow)
{
analogClockSettingsWindow.SettingsChanged -= OnDesktopClockSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is StudyEnvironmentWidgetSettingsWindow studyEnvironmentSettingsWindow)
{
studyEnvironmentSettingsWindow.SettingsChanged -= OnStudyEnvironmentSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is DailyArtworkSettingsWindow dailyArtworkSettingsWindow)
{
dailyArtworkSettingsWindow.SettingsChanged -= OnDailyArtworkSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is WorldClockWidgetSettingsWindow worldClockSettingsWindow)
{
worldClockSettingsWindow.SettingsChanged -= OnWorldClockSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is WeatherWidgetSettingsWindow weatherSettingsWindow)
{
weatherSettingsWindow.SettingsChanged -= OnWeatherSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is CnrDailyNewsSettingsWindow cnrDailyNewsSettingsWindow)
{
cnrDailyNewsSettingsWindow.SettingsChanged -= OnCnrDailyNewsSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is IfengNewsSettingsWindow ifengNewsSettingsWindow)
{
ifengNewsSettingsWindow.SettingsChanged -= OnIfengNewsSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is DailyWordSettingsWindow dailyWordSettingsWindow)
{
dailyWordSettingsWindow.SettingsChanged -= OnDailyWordSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is BilibiliHotSearchSettingsWindow bilibiliHotSearchSettingsWindow)
{
bilibiliHotSearchSettingsWindow.SettingsChanged -= OnBilibiliHotSearchSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is BaiduHotSearchSettingsWindow baiduHotSearchSettingsWindow)
{
baiduHotSearchSettingsWindow.SettingsChanged -= OnBaiduHotSearchSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is Stcn24ForumSettingsWindow stcn24ForumSettingsWindow)
{
stcn24ForumSettingsWindow.SettingsChanged -= OnStcn24ForumSettingsChanged;
}
ComponentSettingsWindow.Opacity = 0;
DispatcherTimer.RunOnce(() =>
{
if (ComponentSettingsWindow is not null)
{
ComponentSettingsWindow.IsVisible = false;
}
if (ComponentSettingsContentHost is not null)
{
ComponentSettingsContentHost.Content = null;
}
}, FluttermotionToken.Slow);
}
// Component settings popup UI is removed in API-only settings hard-cut mode.
private void AddDesktopPage()
{

View File

@@ -147,7 +147,7 @@ public partial class MainWindow
}
}
private void UpdateDesktopSurfaceLayout(GridMetrics gridMetrics)
private void UpdateDesktopSurfaceLayout(DesktopGridMetrics gridMetrics)
{
if (DesktopPagesViewport is null ||
DesktopPagesHost is null ||

View File

@@ -83,10 +83,7 @@ public partial class MainWindow
CloseSettingsPage(immediate: true);
}
if (Application.Current is App app)
{
app.OpenIndependentSettingsModule("MainWindow");
}
AppLogger.Info("SettingsFacade", "Open settings entry is disabled during hard-cut settings API migration.");
}
private void OnCloseSettingsClick(object? sender, RoutedEventArgs e)
@@ -1009,7 +1006,7 @@ public partial class MainWindow
MinShortSideCells,
MaxShortSideCells);
_gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
_gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
_suppressGridSpacingEvents = true;
GridSpacingPresetComboBox.SelectedIndex =
string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;

View File

@@ -312,29 +312,6 @@
</StackPanel>
</Button>
<Button x:Name="OpenSettingsButton"
Grid.Column="1"
Padding="6"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Click="OnOpenSettingsClick"
ToolTip.Tip="&#35774;&#32622;">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="6">
<fi:FluentIcon x:Name="OpenSettingsIcon"
Icon="Settings"
IconVariant="Regular" />
<TextBlock x:Name="OpenSettingsButtonTextBlock"
IsVisible="False"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="&#36820;&#22238;&#26700;&#38754;" />
</StackPanel>
</Button>
</Grid>
</Border>
</Grid>
@@ -521,47 +498,6 @@
</Border>
</Grid>
<Border x:Name="ComponentSettingsWindow"
IsVisible="False"
Opacity="0"
Classes="glass-strong"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Width="400"
MinWidth="300"
MaxWidth="500"
Height="300"
MinHeight="200"
Margin="24,24,24,100"
CornerRadius="36"
Padding="0">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0"
Background="{DynamicResource AdaptiveAccentBrush}"
CornerRadius="36,36,0,0"
Padding="16,12">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Component Settings"
FontSize="16"
FontWeight="SemiBold"
Foreground="White"
VerticalAlignment="Center" />
<Button Grid.Column="1"
Padding="8"
Background="Transparent"
BorderThickness="0"
Click="OnCloseComponentSettingsClick">
<fi:FluentIcon Icon="Dismiss"
FontSize="14"
Foreground="White" />
</Button>
</Grid>
</Border>
<ContentControl x:Name="ComponentSettingsContentHost"
Grid.Row="1" />
</Grid>
</Border>
<Border x:Name="ComponentLibraryWindow"
IsVisible="False"
Opacity="0"

View File

@@ -71,20 +71,9 @@ public partial class MainWindow : Window
};
private static readonly TaskbarActionId[] DefaultPinnedTaskbarActions =
[
TaskbarActionId.MinimizeToWindows,
TaskbarActionId.OpenSettings
TaskbarActionId.MinimizeToWindows
];
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 DesktopGridLayoutService _gridLayoutService = new();
private readonly MonetColorService _monetColorService = new();
private readonly AppSettingsService _appSettingsService = new();
private readonly DesktopLayoutSettingsService _desktopLayoutSettingsService = new();
@@ -300,7 +289,7 @@ public partial class MainWindow : Window
MinShortSideCells,
MaxShortSideCells);
_gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
_gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
_suppressGridSpacingEvents = true;
GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
_suppressGridSpacingEvents = false;
@@ -637,11 +626,11 @@ public partial class MainWindow : Window
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 preset = _gridLayoutService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
var gapRatio = _gridLayoutService.ResolveGapRatio(preset);
var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent);
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset);
var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent);
var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0)
{
return;
@@ -697,7 +686,7 @@ public partial class MainWindow : Window
DrawGridPreviewLines(gridMetrics);
}
private void DrawGridPreviewLines(GridMetrics gridMetrics)
private void DrawGridPreviewLines(DesktopGridMetrics gridMetrics)
{
if (GridPreviewLinesCanvas is null || GridPreviewViewport is null || GridPreviewGrid is null)
{
@@ -778,7 +767,7 @@ public partial class MainWindow : Window
private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e)
{
_gridSpacingPreset = NormalizeGridSpacingPreset(
_gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(
TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
_desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
@@ -836,9 +825,9 @@ public partial class MainWindow : Window
{
var hostWidth = DesktopHost.Bounds.Width;
var hostHeight = DesktopHost.Bounds.Height;
var gapRatio = ResolveGridGapRatio(_gridSpacingPreset);
var edgeInset = CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
var gridMetrics = CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset);
var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset);
var edgeInset = _gridLayoutService.CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
var gridMetrics = _gridLayoutService.CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0)
{
return;
@@ -960,13 +949,6 @@ public partial class MainWindow : Window
insetPx);
}
private static string NormalizeGridSpacingPreset(string? value)
{
return string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase)
? "Compact"
: "Relaxed";
}
private static string NormalizeStatusBarSpacingMode(string? value)
{
return value switch
@@ -987,99 +969,6 @@ public partial class MainWindow : Window
return comboBox?.SelectedItem?.ToString();
}
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;
// Proportional inset based on user percentage selection.
var clampedPercent = Math.Clamp(insetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
var insetRatio = clampedPercent / 100d;
// Keep inset within a practical visual range.
return Math.Clamp(baseCell * insetRatio, 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);
// Edge inset should come only from user setting.
// Remaining free space is handled by container centering, not baked into inset.
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);
}
else
{
var columnCount = shortSide;
var denominator = columnCount + Math.Max(0, columnCount - 1) * clampedGapRatio;
if (denominator <= 0)
{
return default;
}
var cellSize = availableWidth / denominator;
var gapPx = cellSize * clampedGapRatio;
var pitch = cellSize + gapPx;
if (pitch <= 0)
{
return default;
}
var rowCount = Math.Max(1, (int)Math.Floor((availableHeight + 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);
}
}
private static int ClampComponentSpan(int requestedSpan, int axisCellCount)
{
return Math.Clamp(requestedSpan, 1, Math.Max(1, axisCellCount));
@@ -1110,7 +999,6 @@ public partial class MainWindow : Window
var taskbarTextSize = Math.Clamp(taskbarCellHeight * 0.36, 12, 22);
var taskbarIconSize = Math.Clamp(taskbarCellHeight * 0.46, 16, 34);
var taskbarButtonInset = Math.Clamp(taskbarCellHeight * 0.22, 6, 16);
var compactButtonInset = Math.Clamp(taskbarCellHeight * 0.20, 6, 14);
var buttonContentSpacing = Math.Clamp(taskbarCellHeight * 0.20, 6, 14);
var taskbarButtonPadding = new Thickness(taskbarButtonInset);
@@ -1145,27 +1033,6 @@ public partial class MainWindow : Window
OpenComponentLibraryTextBlock.FontSize = taskbarTextSize;
SetButtonContentSpacing(OpenComponentLibraryButton, buttonContentSpacing);
OpenSettingsButton.Margin = new Thickness(0);
OpenSettingsButton.Height = taskbarCellHeight;
OpenSettingsButton.MinHeight = taskbarCellHeight;
OpenSettingsButton.FontSize = taskbarTextSize;
OpenSettingsButtonTextBlock.FontSize = taskbarTextSize;
OpenSettingsIcon.FontSize = taskbarIconSize;
SetButtonContentSpacing(OpenSettingsButton, Math.Clamp(taskbarCellHeight * 0.18, 4, 10));
if (_isSettingsOpen)
{
OpenSettingsButton.Width = double.NaN;
OpenSettingsButton.MinWidth = Math.Clamp(taskbarCellHeight * 2.45, 120, 360);
OpenSettingsButton.Padding = taskbarButtonPadding;
}
else
{
OpenSettingsButton.Width = taskbarCellHeight;
OpenSettingsButton.MinWidth = taskbarCellHeight;
OpenSettingsButton.Padding = new Thickness(compactButtonInset);
}
UpdateComponentLibraryLayout(cellSize);
}
@@ -1293,9 +1160,9 @@ public partial class MainWindow : Window
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);
var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset);
var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0)
{
return;

View File

@@ -1,78 +0,0 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void UpdateComponentsSettingsSummary()
{
if (ComponentsSettingsHubPanel is null)
{
return;
}
var definitions = _componentRegistry
.GetAll()
.OrderBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase)
.ThenBy(definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
var runtime = (Application.Current as App)?.PluginRuntimeService;
var pluginComponentIds = runtime?.DesktopComponents
.Select(contribution => contribution.Registration.ComponentId)
.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? [];
var pluginCount = definitions.Count(definition => pluginComponentIds.Contains(definition.Id));
var builtInCount = definitions.Count - pluginCount;
var desktopCount = definitions.Count(definition => definition.AllowDesktopPlacement);
var statusBarCount = definitions.Count(definition => definition.AllowStatusBarPlacement);
ComponentsSettingsHubPanel.ComponentsSummaryTextBlock.Text = Lf(
"settings.components.summary_format",
"Available components: {0}. Built-in: {1}. Plugin-provided: {2}. Desktop: {3}. Status bar: {4}.",
definitions.Count,
builtInCount,
pluginCount,
desktopCount,
statusBarCount);
ComponentsSettingsHubPanel.ComponentCategoryItemsPanel.Children.Clear();
foreach (var group in definitions
.GroupBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase)
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase))
{
ComponentsSettingsHubPanel.ComponentCategoryItemsPanel.Children.Add(new Border
{
Margin = new Thickness(0, 4, 0, 0),
Padding = new Thickness(12, 10),
Background = GetThemeBrush("LayerFillColorDefaultBrush"),
BorderBrush = GetThemeBrush("CardStrokeColorDefaultBrush"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Child = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 8,
Children =
{
new TextBlock
{
FontWeight = FontWeight.SemiBold,
Text = group.Key
},
new TextBlock
{
Foreground = GetThemeBrush("TextFillColorSecondaryBrush"),
Text = Lf("settings.components.category_count_format", "{0} item(s)", group.Count())
}
}
}
});
}
}
}

View File

@@ -1,261 +0,0 @@
using System;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private string CurrentControlAccessStage => _controlsBound ? "module init" : "control binding";
private T? TryGetOptionalPageControl<T>(Control? pageRoot, string controlName)
where T : Control
{
return pageRoot?.FindControl<T>(controlName);
}
private T RequirePageControl<T>(Control pageRoot, string controlName)
where T : Control
{
var control = pageRoot.FindControl<T>(controlName);
if (control is null)
{
throw new InvalidOperationException(
$"Independent settings module control resolution failed. Page='{pageRoot.Name ?? pageRoot.GetType().Name}'; Control='{controlName}'; Stage='{CurrentControlAccessStage}'.");
}
return control;
}
private T RequireSettingsPage<T>(T? page, string pageName)
where T : Control
{
return page ?? throw new InvalidOperationException(
$"Independent settings module page resolution failed. Page='{pageName}'; Stage='{CurrentControlAccessStage}'.");
}
private WallpaperSettingsPage WallpaperSettingsPageRoot => RequireSettingsPage(WallpaperSettingsPanel, nameof(WallpaperSettingsPanel));
private GridSettingsPage GridSettingsPageRoot => RequireSettingsPage(GridSettingsPanel, nameof(GridSettingsPanel));
private ColorSettingsPage ColorSettingsPageRoot => RequireSettingsPage(ColorSettingsPanel, nameof(ColorSettingsPanel));
private StatusBarSettingsPage StatusBarSettingsPageRoot => RequireSettingsPage(StatusBarSettingsPanel, nameof(StatusBarSettingsPanel));
private RegionSettingsPage RegionSettingsPageRoot => RequireSettingsPage(RegionSettingsPanel, nameof(RegionSettingsPanel));
private WeatherSettingsPage WeatherSettingsPageRoot => RequireSettingsPage(WeatherSettingsPanel, nameof(WeatherSettingsPanel));
private UpdateSettingsPage UpdateSettingsPageRoot => RequireSettingsPage(UpdateSettingsPanel, nameof(UpdateSettingsPanel));
private AboutSettingsPage AboutSettingsPageRoot => RequireSettingsPage(AboutSettingsPanel, nameof(AboutSettingsPanel));
private LauncherSettingsPage LauncherSettingsPageRoot => RequireSettingsPage(LauncherSettingsPanel, nameof(LauncherSettingsPanel));
private T WallpaperControl<T>(string name) where T : Control => RequirePageControl<T>(WallpaperSettingsPageRoot, name);
private T GridControl<T>(string name) where T : Control => RequirePageControl<T>(GridSettingsPageRoot, name);
private T ColorControl<T>(string name) where T : Control => RequirePageControl<T>(ColorSettingsPageRoot, name);
private T StatusBarControl<T>(string name) where T : Control => RequirePageControl<T>(StatusBarSettingsPageRoot, name);
private T RegionControl<T>(string name) where T : Control => RequirePageControl<T>(RegionSettingsPageRoot, name);
private T WeatherControl<T>(string name) where T : Control => RequirePageControl<T>(WeatherSettingsPageRoot, name);
private T UpdateControl<T>(string name) where T : Control => RequirePageControl<T>(UpdateSettingsPageRoot, name);
private T AboutControl<T>(string name) where T : Control => RequirePageControl<T>(AboutSettingsPageRoot, name);
private T LauncherControl<T>(string name) where T : Control => RequirePageControl<T>(LauncherSettingsPageRoot, name);
internal TextBlock WallpaperPanelTitleTextBlock => WallpaperControl<TextBlock>("WallpaperPanelTitleTextBlock");
internal TextBlock WallpaperPathTextBlock => WallpaperControl<TextBlock>("WallpaperPathTextBlock");
internal TextBlock WallpaperStatusTextBlock => WallpaperControl<TextBlock>("WallpaperStatusTextBlock");
internal ComboBox WallpaperPlacementComboBox => WallpaperControl<ComboBox>("WallpaperPlacementComboBox");
internal Border WallpaperPreviewHost => WallpaperControl<Border>("WallpaperPreviewHost");
internal Border WallpaperPreviewFrame => WallpaperControl<Border>("WallpaperPreviewFrame");
internal Border WallpaperPreviewViewport => WallpaperControl<Border>("WallpaperPreviewViewport");
internal Image WallpaperPreviewVideoImage => WallpaperControl<Image>("WallpaperPreviewVideoImage");
internal Grid WallpaperPreviewGrid => WallpaperControl<Grid>("WallpaperPreviewGrid");
internal Border WallpaperPreviewTopStatusBarHost => WallpaperControl<Border>("WallpaperPreviewTopStatusBarHost");
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel");
internal ClockWidget WallpaperPreviewClockWidget => WallpaperControl<ClockWidget>("WallpaperPreviewClockWidget");
internal Border WallpaperPreviewBottomTaskbarContainer => WallpaperControl<Border>("WallpaperPreviewBottomTaskbarContainer");
internal Border WallpaperPreviewTaskbarFixedActionsHost => WallpaperControl<Border>("WallpaperPreviewTaskbarFixedActionsHost");
internal StackPanel WallpaperPreviewBackButtonVisual => WallpaperControl<StackPanel>("WallpaperPreviewBackButtonVisual");
internal TextBlock WallpaperPreviewBackButtonTextBlock => WallpaperControl<TextBlock>("WallpaperPreviewBackButtonTextBlock");
internal StackPanel WallpaperPreviewTaskbarDynamicActionsHost => WallpaperControl<StackPanel>("WallpaperPreviewTaskbarDynamicActionsHost");
internal Border WallpaperPreviewTaskbarSettingsActionHost => WallpaperControl<Border>("WallpaperPreviewTaskbarSettingsActionHost");
internal StackPanel WallpaperPreviewComponentLibraryVisual => WallpaperControl<StackPanel>("WallpaperPreviewComponentLibraryVisual");
internal TextBlock WallpaperPreviewComponentLibraryTextBlock => WallpaperControl<TextBlock>("WallpaperPreviewComponentLibraryTextBlock");
internal FluentIcons.Avalonia.SymbolIcon WallpaperPreviewSettingsButtonIcon => WallpaperControl<FluentIcons.Avalonia.SymbolIcon>("WallpaperPreviewSettingsButtonIcon");
internal Button PickWallpaperButton => WallpaperControl<Button>("PickWallpaperButton");
internal Button ClearWallpaperButton => WallpaperControl<Button>("ClearWallpaperButton");
internal FluentAvalonia.UI.Controls.SettingsExpander WallpaperPlacementSettingsExpander => WallpaperControl<FluentAvalonia.UI.Controls.SettingsExpander>("WallpaperPlacementSettingsExpander");
private Image? OptionalWallpaperPreviewVideoImage => TryGetOptionalPageControl<Image>(WallpaperSettingsPanel, "WallpaperPreviewVideoImage");
private Border? OptionalWallpaperPreviewViewport => TryGetOptionalPageControl<Border>(WallpaperSettingsPanel, "WallpaperPreviewViewport");
private bool IsWallpaperSettingsPageVisible => string.Equals(NormalizeSettingsPageTag(_selectedSettingsTabTag), "Appearance", StringComparison.OrdinalIgnoreCase);
internal TextBlock GridPanelTitleTextBlock => GridControl<TextBlock>("GridPanelTitleTextBlock");
internal Border GridPreviewHost => GridControl<Border>("GridPreviewHost");
internal Border GridPreviewFrame => GridControl<Border>("GridPreviewFrame");
internal Border GridPreviewViewport => GridControl<Border>("GridPreviewViewport");
internal Canvas GridPreviewLinesCanvas => GridControl<Canvas>("GridPreviewLinesCanvas");
internal Grid GridPreviewGrid => GridControl<Grid>("GridPreviewGrid");
internal Border GridPreviewTopStatusBarHost => GridControl<Border>("GridPreviewTopStatusBarHost");
internal StackPanel GridPreviewTopStatusComponentsPanel => GridControl<StackPanel>("GridPreviewTopStatusComponentsPanel");
internal Border GridPreviewBottomTaskbarContainer => GridControl<Border>("GridPreviewBottomTaskbarContainer");
internal Border GridPreviewTaskbarFixedActionsHost => GridControl<Border>("GridPreviewTaskbarFixedActionsHost");
internal StackPanel GridPreviewBackButtonVisual => GridControl<StackPanel>("GridPreviewBackButtonVisual");
internal TextBlock GridPreviewBackButtonTextBlock => GridControl<TextBlock>("GridPreviewBackButtonTextBlock");
internal StackPanel GridPreviewTaskbarDynamicActionsHost => GridControl<StackPanel>("GridPreviewTaskbarDynamicActionsHost");
internal Border GridPreviewTaskbarSettingsActionHost => GridControl<Border>("GridPreviewTaskbarSettingsActionHost");
internal StackPanel GridPreviewComponentLibraryVisual => GridControl<StackPanel>("GridPreviewComponentLibraryVisual");
internal FluentIcons.Avalonia.FluentIcon GridPreviewComponentLibraryIcon => GridControl<FluentIcons.Avalonia.FluentIcon>("GridPreviewComponentLibraryIcon");
internal TextBlock GridPreviewComponentLibraryTextBlock => GridControl<TextBlock>("GridPreviewComponentLibraryTextBlock");
internal FluentIcons.Avalonia.SymbolIcon GridPreviewSettingsButtonIcon => GridControl<FluentIcons.Avalonia.SymbolIcon>("GridPreviewSettingsButtonIcon");
internal FluentAvalonia.UI.Controls.SettingsExpander GridRowsSettingsExpander => GridControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridRowsSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander GridSpacingSettingsExpander => GridControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridSpacingSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander GridEdgeInsetSettingsExpander => GridControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridEdgeInsetSettingsExpander");
internal Slider GridSizeSlider => GridControl<Slider>("GridSizeSlider");
internal FluentAvalonia.UI.Controls.NumberBox GridSizeNumberBox => GridControl<FluentAvalonia.UI.Controls.NumberBox>("GridSizeNumberBox");
internal ComboBox GridSpacingPresetComboBox => GridControl<ComboBox>("GridSpacingPresetComboBox");
internal ComboBoxItem GridSpacingRelaxedComboBoxItem => GridControl<ComboBoxItem>("GridSpacingRelaxedComboBoxItem");
internal ComboBoxItem GridSpacingCompactComboBoxItem => GridControl<ComboBoxItem>("GridSpacingCompactComboBoxItem");
internal Slider GridEdgeInsetSlider => GridControl<Slider>("GridEdgeInsetSlider");
internal FluentAvalonia.UI.Controls.NumberBox GridEdgeInsetNumberBox => GridControl<FluentAvalonia.UI.Controls.NumberBox>("GridEdgeInsetNumberBox");
internal TextBlock GridEdgeInsetComputedPxTextBlock => GridControl<TextBlock>("GridEdgeInsetComputedPxTextBlock");
internal Button ApplyGridButton => GridControl<Button>("ApplyGridButton");
internal TextBlock GridInfoTextBlock => GridControl<TextBlock>("GridInfoTextBlock");
internal TextBlock ColorPanelTitleTextBlock => ColorControl<TextBlock>("ColorPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeModeSettingsExpander => ColorControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeModeSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeColorSettingsExpander => ColorControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeColorSettingsExpander");
internal ToggleSwitch NightModeToggleSwitch => ColorControl<ToggleSwitch>("NightModeToggleSwitch");
internal TextBlock ThemeColorStatusTextBlock => ColorControl<TextBlock>("ThemeColorStatusTextBlock");
internal TextBlock RecommendedColorsLabelTextBlock => ColorControl<TextBlock>("RecommendedColorsLabelTextBlock");
internal TextBlock SystemMonetColorsLabelTextBlock => ColorControl<TextBlock>("SystemMonetColorsLabelTextBlock");
internal Button RecommendedColorButton1 => ColorControl<Button>("RecommendedColorButton1");
internal Button RecommendedColorButton2 => ColorControl<Button>("RecommendedColorButton2");
internal Button RecommendedColorButton3 => ColorControl<Button>("RecommendedColorButton3");
internal Button RecommendedColorButton4 => ColorControl<Button>("RecommendedColorButton4");
internal Button RecommendedColorButton5 => ColorControl<Button>("RecommendedColorButton5");
internal Button RecommendedColorButton6 => ColorControl<Button>("RecommendedColorButton6");
internal Border RecommendedColorSwatch1 => ColorControl<Border>("RecommendedColorSwatch1");
internal Border RecommendedColorSwatch2 => ColorControl<Border>("RecommendedColorSwatch2");
internal Border RecommendedColorSwatch3 => ColorControl<Border>("RecommendedColorSwatch3");
internal Border RecommendedColorSwatch4 => ColorControl<Border>("RecommendedColorSwatch4");
internal Border RecommendedColorSwatch5 => ColorControl<Border>("RecommendedColorSwatch5");
internal Border RecommendedColorSwatch6 => ColorControl<Border>("RecommendedColorSwatch6");
internal Button RefreshMonetColorsButton => ColorControl<Button>("RefreshMonetColorsButton");
internal Button MonetColorButton1 => ColorControl<Button>("MonetColorButton1");
internal Button MonetColorButton2 => ColorControl<Button>("MonetColorButton2");
internal Button MonetColorButton3 => ColorControl<Button>("MonetColorButton3");
internal Button MonetColorButton4 => ColorControl<Button>("MonetColorButton4");
internal Button MonetColorButton5 => ColorControl<Button>("MonetColorButton5");
internal Button MonetColorButton6 => ColorControl<Button>("MonetColorButton6");
internal Border MonetColorSwatch1 => ColorControl<Border>("MonetColorSwatch1");
internal Border MonetColorSwatch2 => ColorControl<Border>("MonetColorSwatch2");
internal Border MonetColorSwatch3 => ColorControl<Border>("MonetColorSwatch3");
internal Border MonetColorSwatch4 => ColorControl<Border>("MonetColorSwatch4");
internal Border MonetColorSwatch5 => ColorControl<Border>("MonetColorSwatch5");
internal Border MonetColorSwatch6 => ColorControl<Border>("MonetColorSwatch6");
internal TextBlock StatusBarPanelTitleTextBlock => StatusBarControl<TextBlock>("StatusBarPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarClockSettingsExpander => StatusBarControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarClockSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarSpacingSettingsExpander => StatusBarControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarSpacingSettingsExpander");
internal ToggleSwitch StatusBarClockToggleSwitch => StatusBarControl<ToggleSwitch>("StatusBarClockToggleSwitch");
internal RadioButton ClockFormatHMSSRadio => StatusBarControl<RadioButton>("ClockFormatHMSSRadio");
internal RadioButton ClockFormatHMRadio => StatusBarControl<RadioButton>("ClockFormatHMRadio");
internal ComboBox StatusBarSpacingModeComboBox => StatusBarControl<ComboBox>("StatusBarSpacingModeComboBox");
internal ComboBoxItem StatusBarSpacingModeCompactItem => StatusBarControl<ComboBoxItem>("StatusBarSpacingModeCompactItem");
internal ComboBoxItem StatusBarSpacingModeRelaxedItem => StatusBarControl<ComboBoxItem>("StatusBarSpacingModeRelaxedItem");
internal ComboBoxItem StatusBarSpacingModeCustomItem => StatusBarControl<ComboBoxItem>("StatusBarSpacingModeCustomItem");
internal FluentAvalonia.UI.Controls.SettingsExpanderItem StatusBarSpacingCustomPanel => StatusBarControl<FluentAvalonia.UI.Controls.SettingsExpanderItem>("StatusBarSpacingCustomPanel");
internal Slider StatusBarSpacingSlider => StatusBarControl<Slider>("StatusBarSpacingSlider");
internal FluentAvalonia.UI.Controls.NumberBox StatusBarSpacingNumberBox => StatusBarControl<FluentAvalonia.UI.Controls.NumberBox>("StatusBarSpacingNumberBox");
internal TextBlock StatusBarSpacingComputedPxTextBlock => StatusBarControl<TextBlock>("StatusBarSpacingComputedPxTextBlock");
internal TextBlock RegionPanelTitleTextBlock => RegionControl<TextBlock>("RegionPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander LanguageSettingsExpander => RegionControl<FluentAvalonia.UI.Controls.SettingsExpander>("LanguageSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander TimeZoneSettingsExpander => RegionControl<FluentAvalonia.UI.Controls.SettingsExpander>("TimeZoneSettingsExpander");
internal ComboBox LanguageComboBox => RegionControl<ComboBox>("LanguageComboBox");
internal ComboBoxItem LanguageChineseItem => RegionControl<ComboBoxItem>("LanguageChineseItem");
internal ComboBoxItem LanguageEnglishItem => RegionControl<ComboBoxItem>("LanguageEnglishItem");
internal ComboBox TimeZoneComboBox => RegionControl<ComboBox>("TimeZoneComboBox");
internal TextBlock WeatherPanelTitleTextBlock => WeatherControl<TextBlock>("WeatherPanelTitleTextBlock");
internal TextBlock WeatherPreviewSectionTextBlock => WeatherControl<TextBlock>("WeatherPreviewSectionTextBlock");
internal TextBlock WeatherSettingsSectionTextBlock => WeatherControl<TextBlock>("WeatherSettingsSectionTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherPreviewSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherPreviewSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherLocationSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherLocationSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCitySearchSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCitySearchSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCoordinateSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCoordinateSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherAlertFilterSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherAlertFilterSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherIconPackSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherIconPackSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherNoTlsSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherNoTlsSettingsExpander");
internal Button WeatherPreviewButton => WeatherControl<Button>("WeatherPreviewButton");
internal ComboBox WeatherLocationModeComboBox => WeatherControl<ComboBox>("WeatherLocationModeComboBox");
internal ComboBoxItem WeatherLocationModeCityItem => WeatherControl<ComboBoxItem>("WeatherLocationModeCityItem");
internal ComboBoxItem WeatherLocationModeCoordinatesItem => WeatherControl<ComboBoxItem>("WeatherLocationModeCoordinatesItem");
internal ListBoxItem WeatherLocationModeCityChipItem => WeatherControl<ListBoxItem>("WeatherLocationModeCityChipItem");
internal ListBoxItem WeatherLocationModeCoordinatesChipItem => WeatherControl<ListBoxItem>("WeatherLocationModeCoordinatesChipItem");
internal ListBox WeatherLocationModeChipListBox => WeatherControl<ListBox>("WeatherLocationModeChipListBox");
internal ToggleSwitch WeatherAutoRefreshToggleSwitch => WeatherControl<ToggleSwitch>("WeatherAutoRefreshToggleSwitch");
internal Button WeatherSearchButton => WeatherControl<Button>("WeatherSearchButton");
internal Button WeatherApplyCityButton => WeatherControl<Button>("WeatherApplyCityButton");
internal Button WeatherApplyCoordinatesButton => WeatherControl<Button>("WeatherApplyCoordinatesButton");
internal TextBox WeatherExcludedAlertsTextBox => WeatherControl<TextBox>("WeatherExcludedAlertsTextBox");
internal ComboBox WeatherIconPackComboBox => WeatherControl<ComboBox>("WeatherIconPackComboBox");
internal ToggleSwitch WeatherNoTlsToggleSwitch => WeatherControl<ToggleSwitch>("WeatherNoTlsToggleSwitch");
internal TextBox WeatherCitySearchTextBox => WeatherControl<TextBox>("WeatherCitySearchTextBox");
internal ComboBox WeatherCityResultsComboBox => WeatherControl<ComboBox>("WeatherCityResultsComboBox");
internal TextBlock WeatherSearchStatusTextBlock => WeatherControl<TextBlock>("WeatherSearchStatusTextBlock");
internal TextBox WeatherLocationKeyTextBox => WeatherControl<TextBox>("WeatherLocationKeyTextBox");
internal TextBox WeatherLocationNameTextBox => WeatherControl<TextBox>("WeatherLocationNameTextBox");
internal FluentAvalonia.UI.Controls.NumberBox WeatherLatitudeNumberBox => WeatherControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLatitudeNumberBox");
internal FluentAvalonia.UI.Controls.NumberBox WeatherLongitudeNumberBox => WeatherControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLongitudeNumberBox");
internal TextBlock WeatherCoordinateStatusTextBlock => WeatherControl<TextBlock>("WeatherCoordinateStatusTextBlock");
internal TextBlock WeatherPreviewResultTextBlock => WeatherControl<TextBlock>("WeatherPreviewResultTextBlock");
internal Image WeatherPreviewIconImage => WeatherControl<Image>("WeatherPreviewIconImage");
internal FluentIcons.Avalonia.Fluent.SymbolIcon WeatherPreviewIconSymbol => WeatherControl<FluentIcons.Avalonia.Fluent.SymbolIcon>("WeatherPreviewIconSymbol");
internal TextBlock WeatherPreviewTemperatureTextBlock => WeatherControl<TextBlock>("WeatherPreviewTemperatureTextBlock");
internal TextBlock WeatherPreviewUpdatedTextBlock => WeatherControl<TextBlock>("WeatherPreviewUpdatedTextBlock");
internal FluentAvalonia.UI.Controls.ProgressRing WeatherSearchProgressRing => WeatherControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherSearchProgressRing");
internal FluentAvalonia.UI.Controls.ProgressRing WeatherPreviewProgressRing => WeatherControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherPreviewProgressRing");
internal ComboBoxItem WeatherIconPackFluentRegularItem => WeatherControl<ComboBoxItem>("WeatherIconPackFluentRegularItem");
internal ComboBoxItem WeatherIconPackFluentFilledItem => WeatherControl<ComboBoxItem>("WeatherIconPackFluentFilledItem");
internal TextBlock WeatherLocationSelectionTitleTextBlock => WeatherControl<TextBlock>("WeatherLocationSelectionTitleTextBlock");
internal TextBlock WeatherLocationSelectionDescriptionTextBlock => WeatherControl<TextBlock>("WeatherLocationSelectionDescriptionTextBlock");
internal TextBlock WeatherLocationValueTextBlock => WeatherControl<TextBlock>("WeatherLocationValueTextBlock");
internal TextBlock WeatherLocationStatusTextBlock => WeatherControl<TextBlock>("WeatherLocationStatusTextBlock");
internal TextBlock WeatherAlertListTitleTextBlock => WeatherControl<TextBlock>("WeatherAlertListTitleTextBlock");
internal TextBlock WeatherAlertListDescriptionTextBlock => WeatherControl<TextBlock>("WeatherAlertListDescriptionTextBlock");
internal TextBlock WeatherFooterHintTextBlock => WeatherControl<TextBlock>("WeatherFooterHintTextBlock");
internal TextBlock UpdatePanelTitleTextBlock => UpdateControl<TextBlock>("UpdatePanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateOptionsSettingsExpander => UpdateControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateOptionsSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateActionsSettingsExpander => UpdateControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateActionsSettingsExpander");
internal TextBlock UpdateCurrentVersionLabelTextBlock => UpdateControl<TextBlock>("UpdateCurrentVersionLabelTextBlock");
internal TextBlock UpdateCurrentVersionValueTextBlock => UpdateControl<TextBlock>("UpdateCurrentVersionValueTextBlock");
internal TextBlock UpdateLatestVersionLabelTextBlock => UpdateControl<TextBlock>("UpdateLatestVersionLabelTextBlock");
internal TextBlock UpdateLatestVersionValueTextBlock => UpdateControl<TextBlock>("UpdateLatestVersionValueTextBlock");
internal TextBlock UpdatePublishedAtLabelTextBlock => UpdateControl<TextBlock>("UpdatePublishedAtLabelTextBlock");
internal TextBlock UpdatePublishedAtValueTextBlock => UpdateControl<TextBlock>("UpdatePublishedAtValueTextBlock");
internal TextBlock UpdateChannelLabelTextBlock => UpdateControl<TextBlock>("UpdateChannelLabelTextBlock");
internal ListBoxItem UpdateChannelStableChipItem => UpdateControl<ListBoxItem>("UpdateChannelStableChipItem");
internal ListBoxItem UpdateChannelPreviewChipItem => UpdateControl<ListBoxItem>("UpdateChannelPreviewChipItem");
internal ToggleSwitch AutoCheckUpdatesToggleSwitch => UpdateControl<ToggleSwitch>("AutoCheckUpdatesToggleSwitch");
internal ListBox UpdateChannelChipListBox => UpdateControl<ListBox>("UpdateChannelChipListBox");
internal Button CheckForUpdatesButton => UpdateControl<Button>("CheckForUpdatesButton");
internal Button DownloadAndInstallUpdateButton => UpdateControl<Button>("DownloadAndInstallUpdateButton");
internal ProgressBar UpdateDownloadProgressBar => UpdateControl<ProgressBar>("UpdateDownloadProgressBar");
internal TextBlock UpdateDownloadProgressTextBlock => UpdateControl<TextBlock>("UpdateDownloadProgressTextBlock");
internal TextBlock UpdateStatusTextBlock => UpdateControl<TextBlock>("UpdateStatusTextBlock");
internal TextBlock AboutPanelTitleTextBlock => AboutControl<TextBlock>("AboutPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander AboutStartupSettingsExpander => AboutControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutStartupSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander AboutRenderModeSettingsExpander => AboutControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutRenderModeSettingsExpander");
internal ToggleSwitch AutoStartWithWindowsToggleSwitch => AboutControl<ToggleSwitch>("AutoStartWithWindowsToggleSwitch");
internal ComboBox AppRenderModeComboBox => AboutControl<ComboBox>("AppRenderModeComboBox");
internal TextBlock CurrentRenderBackendLabelTextBlock => AboutControl<TextBlock>("CurrentRenderBackendLabelTextBlock");
internal TextBlock CurrentRenderBackendValueTextBlock => AboutControl<TextBlock>("CurrentRenderBackendValueTextBlock");
internal TextBlock CurrentRenderBackendImplementationTextBlock => AboutControl<TextBlock>("CurrentRenderBackendImplementationTextBlock");
internal TextBlock VersionTextBlock => AboutControl<TextBlock>("VersionTextBlock");
internal TextBlock CodeNameTextBlock => AboutControl<TextBlock>("CodeNameTextBlock");
internal TextBlock FontInfoTextBlock => AboutControl<TextBlock>("FontInfoTextBlock");
internal TextBlock LauncherSettingsPanelTitleTextBlock => LauncherControl<TextBlock>("LauncherSettingsPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander LauncherHiddenItemsSettingsExpander => LauncherControl<FluentAvalonia.UI.Controls.SettingsExpander>("LauncherHiddenItemsSettingsExpander");
internal TextBlock LauncherHiddenItemsEmptyTextBlock => LauncherControl<TextBlock>("LauncherHiddenItemsEmptyTextBlock");
internal TextBlock LauncherHiddenItemsDescriptionTextBlock => LauncherControl<TextBlock>("LauncherHiddenItemsDescriptionTextBlock");
}

View File

@@ -1,857 +0,0 @@
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 FluentAvalonia.UI.Controls;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Views;
using FluentIconVariant = FluentIcons.Common.IconVariant;
using FluentSymbol = FluentIcons.Common.Symbol;
using FluentSymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource;
public partial class SettingsWindow
{
private readonly Dictionary<string, Control> _builtInSettingsPageHosts = new(StringComparer.OrdinalIgnoreCase);
internal void Open(string? pageTag = null)
{
if (!string.IsNullOrWhiteSpace(pageTag))
{
_selectedSettingsTabTag = NormalizeSettingsPageTag(pageTag);
if (_independentModuleInitializationCompleted)
{
SelectSettingsTab(_selectedSettingsTabTag, persistSelection: false);
}
}
if (!IsVisible)
{
Show();
}
if (WindowState == WindowState.Minimized)
{
WindowState = WindowState.Normal;
}
Activate();
}
internal void PrepareForForceClose()
{
_allowIndependentSettingsModuleRealClose = true;
}
protected override void OnClosed(EventArgs e)
{
AppLogger.Info(
"IndependentSettingsModule",
$"PreviewCleanupStarted; Stage='WindowCloseCleanup'; Module='WallpaperPreview'; CloseRequested={_isIndependentSettingsModuleClosing}.");
try
{
_persistSettingsDebounceTimer?.Dispose();
_persistSettingsDebounceTimer = null;
StopVideoWallpaper();
_previewVideoWallpaperPlayer?.Dispose();
_previewVideoWallpaperPlayer = null;
_previewVideoWallpaperMedia?.Dispose();
_previewVideoWallpaperMedia = null;
_previewVideoFrameRefreshTimer?.Stop();
_previewVideoFrameRefreshTimer = 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();
AppLogger.Info(
"IndependentSettingsModule",
$"PreviewCleanupCompleted; Stage='WindowCloseCleanup'; Module='WallpaperPreview'; CloseRequested={_isIndependentSettingsModuleClosing}.");
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
AppLogger.Warn(
"IndependentSettingsModule",
$"PreviewCleanupFailed; Stage='WindowCloseCleanup'; Module='WallpaperPreview'; Downgraded=True; CloseRequested={_isIndependentSettingsModuleClosing}.",
ex);
}
finally
{
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
Closing -= OnIndependentSettingsModuleClosing;
base.OnClosed(e);
AppLogger.Info("IndependentSettingsModule", $"WindowClosed; CloseRequested={_isIndependentSettingsModuleClosing}.");
_isIndependentSettingsModuleClosing = false;
_allowIndependentSettingsModuleRealClose = false;
}
}
private void InitializeSettingsNavigation()
{
_settingsPageDefinitions.Clear();
_settingsNavItems.Clear();
InitializePluginSettingsNavigation();
RegisterBuiltInSettingsPageDefinitions();
RegisterPluginSettingsDefinitions();
RebuildSettingsNavigationMenu();
}
private void RegisterBuiltInSettingsPageDefinitions()
{
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"General",
L("settings.nav.general", "General"),
L("settings.page_desc.general", "Manage language, launcher, and weather behavior from the independent settings module."),
FluentSymbol.Settings,
IndependentSettingsPageCategory.Internal,
0));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"Appearance",
L("settings.nav.appearance", "Appearance"),
L("settings.page_desc.appearance", "Personalize wallpaper, desktop grid, and accent colors in one place."),
FluentSymbol.PaintBrush,
IndependentSettingsPageCategory.Internal,
10));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"Components",
L("settings.nav.components", "Components"),
L("settings.page_desc.components", "Review available desktop components and configure the status bar area."),
FluentSymbol.Apps,
IndependentSettingsPageCategory.Internal,
20));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"Update",
L("settings.nav.update", "Update"),
L("settings.page_desc.update", "Check for updates and control the update channel."),
FluentSymbol.ArrowSync,
IndependentSettingsPageCategory.Internal,
30));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"Plugins",
L("settings.nav.plugins", "Plugins"),
L("settings.page_desc.plugins", "Review installed plugins, runtime state, and local package installation."),
FluentSymbol.PuzzlePiece,
IndependentSettingsPageCategory.External,
100));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"PluginMarket",
L("settings.nav.plugin_market", "Plugin Market"),
L("settings.page_desc.pluginmarket", "Browse the official plugin market and stage installs safely."),
FluentSymbol.ShoppingBag,
IndependentSettingsPageCategory.External,
110));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"About",
L("settings.nav.about", "About"),
L("settings.page_desc.about", "See version information, rendering backend, and startup behavior."),
FluentSymbol.Info,
IndependentSettingsPageCategory.About,
200));
}
private void RegisterSettingsPageDefinition(IndependentSettingsPageDefinition definition)
{
_settingsPageDefinitions[definition.Tag] = definition;
}
private void RebuildSettingsNavigationMenu()
{
if (SettingsNavView is null)
{
return;
}
var selectedTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
SettingsNavView.MenuItems.Clear();
_settingsNavItems.Clear();
_pluginSettingsNavItems.Clear();
IndependentSettingsPageCategory? lastCategory = null;
foreach (var definition in _settingsPageDefinitions.Values
.OrderBy(definition => GetSettingsPageCategoryOrder(definition.Category))
.ThenBy(definition => definition.SortOrder)
.ThenBy(definition => definition.Title, StringComparer.CurrentCulture))
{
if (lastCategory is not null && lastCategory != definition.Category)
{
SettingsNavView.MenuItems.Add(new NavigationViewItemSeparator());
}
var navItem = CreateSettingsNavItem(definition);
SettingsNavView.MenuItems.Add(navItem);
_settingsNavItems[definition.Tag] = navItem;
if (definition.Category == IndependentSettingsPageCategory.External)
{
_pluginSettingsNavItems[definition.Tag] = navItem;
}
lastCategory = definition.Category;
}
if (_settingsNavItems.TryGetValue(selectedTag, out var selectedItem))
{
SettingsNavView.SelectedItem = selectedItem;
return;
}
if (SettingsNavView.MenuItems.OfType<NavigationViewItem>().FirstOrDefault() is { } firstItem)
{
_selectedSettingsTabTag = firstItem.Tag?.ToString() ?? "General";
SettingsNavView.SelectedItem = firstItem;
}
}
private NavigationViewItem CreateSettingsNavItem(IndependentSettingsPageDefinition definition)
{
var item = new NavigationViewItem
{
Content = definition.Title,
Tag = definition.Tag,
IconSource = new FluentSymbolIconSource
{
Symbol = definition.Icon,
IconVariant = FluentIconVariant.Regular
}
};
if (!string.IsNullOrWhiteSpace(definition.ToolTip))
{
ToolTip.SetTip(item, definition.ToolTip);
}
return item;
}
private static int GetSettingsPageCategoryOrder(IndependentSettingsPageCategory category)
{
return category switch
{
IndependentSettingsPageCategory.Internal => 0,
IndependentSettingsPageCategory.External => 1,
IndependentSettingsPageCategory.About => 2,
IndependentSettingsPageCategory.Debug => 3,
_ => int.MaxValue
};
}
private void InitializeSettingsPageHosts()
{
_builtInSettingsPageHosts.Clear();
GeneralSettingsHubPanel = new GeneralSettingsPage();
AppearanceSettingsHubPanel = new AppearanceSettingsPage();
ComponentsSettingsHubPanel = new ComponentsSettingsPage();
WallpaperSettingsPanel = new WallpaperSettingsPage();
GridSettingsPanel = new GridSettingsPage();
ColorSettingsPanel = new ColorSettingsPage();
StatusBarSettingsPanel = new StatusBarSettingsPage();
WeatherSettingsPanel = new WeatherSettingsPage();
RegionSettingsPanel = new RegionSettingsPage();
UpdateSettingsPanel = new UpdateSettingsPage();
LauncherSettingsPanel = new LauncherSettingsPage();
AboutSettingsPanel = new AboutSettingsPage();
PluginSettingsPanel = new PluginSettingsPage();
PluginMarketSettingsPanel = new PluginMarketSettingsPage();
GeneralSettingsHubPanel.RegionContentHost.Content = RegionSettingsPanel;
GeneralSettingsHubPanel.LauncherContentHost.Content = LauncherSettingsPanel;
GeneralSettingsHubPanel.WeatherContentHost.Content = WeatherSettingsPanel;
AppearanceSettingsHubPanel.WallpaperContentHost.Content = WallpaperSettingsPanel;
AppearanceSettingsHubPanel.GridContentHost.Content = GridSettingsPanel;
AppearanceSettingsHubPanel.ColorContentHost.Content = ColorSettingsPanel;
ComponentsSettingsHubPanel.StatusBarContentHost.Content = StatusBarSettingsPanel;
RegisterBuiltInSettingsPage("General", GeneralSettingsHubPanel);
RegisterBuiltInSettingsPage("Appearance", AppearanceSettingsHubPanel);
RegisterBuiltInSettingsPage("Components", ComponentsSettingsHubPanel);
RegisterBuiltInSettingsPage("Update", UpdateSettingsPanel);
RegisterBuiltInSettingsPage("About", AboutSettingsPanel);
RegisterBuiltInSettingsPage("Plugins", PluginSettingsPanel);
RegisterBuiltInSettingsPage("PluginMarket", PluginMarketSettingsPanel);
}
private void RegisterBuiltInSettingsPage(string tag, Control? page)
{
if (page is not null)
{
_builtInSettingsPageHosts[tag] = page;
}
}
private Control? ResolveSettingsPageHost(string? tag)
{
if (string.IsNullOrWhiteSpace(tag))
{
return null;
}
if (_builtInSettingsPageHosts.TryGetValue(tag, out var builtIn))
{
return builtIn;
}
return _pluginSettingsPageHosts.GetValueOrDefault(tag);
}
private void OnSettingsNavSelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e)
{
if (e.SelectedItem is NavigationViewItem selectedItem &&
selectedItem.Tag is not null)
{
_selectedSettingsTabTag = NormalizeSettingsPageTag(selectedItem.Tag.ToString());
}
AppLogger.Info("IndependentSettingsModule", $"NavigationChanged; Tag='{_selectedSettingsTabTag}'.");
UpdateSettingsTabContent();
SchedulePersistSettings(0);
}
private NavigationViewItem? GetSettingsNavItem(string tag)
{
if (_settingsNavItems.TryGetValue(tag, out var builtIn))
{
return builtIn;
}
return _pluginSettingsNavItems.GetValueOrDefault(tag);
}
private void SelectSettingsTab(string? tag, bool persistSelection)
{
if (string.IsNullOrWhiteSpace(tag) || SettingsNavView is null)
{
return;
}
if (GetSettingsNavItem(tag) is not { } selectedItem)
{
return;
}
_selectedSettingsTabTag = tag;
SettingsNavView.SelectedItem = selectedItem;
UpdateSettingsTabContent();
if (persistSelection)
{
PersistSettings();
}
}
private int GetSettingsTabIndex()
{
return ResolveSelectedSettingsTabIndex();
}
private void UpdateSettingsTabContent()
{
if (SettingsNavView is null || SettingsPageFrame is null)
{
return;
}
var tag = GetSelectedSettingsTabTag();
UpdateCurrentSettingsPageHeader(tag);
if (ResolveSettingsPageHost(tag) is { } pageHost)
{
if (!ReferenceEquals(SettingsPageFrame.Content, pageHost))
{
SettingsPageFrame.Content = pageHost;
}
}
else
{
AppLogger.Warn("IndependentSettingsModule", $"PageHostMissing; Tag='{tag}'.");
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能加载"),
$"No settings page host is registered for '{tag}'.",
InfoBarSeverity.Warning);
return;
}
if (tag == "General")
{
RenderLauncherHiddenItemsList();
}
if (tag == "Plugins")
{
PluginSettingsPanel.RefreshFromRuntime();
}
if (tag == "PluginMarket")
{
PluginMarketSettingsPanel.RefreshFromRuntime();
}
if (tag == "Appearance")
{
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
}
if (tag == "Components")
{
UpdateComponentsSettingsSummary();
}
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
SyncVideoWallpaperPreviewPlayback();
}
private void PersistSettings()
{
if (_suppressSettingsPersistence)
{
return;
}
AppLogger.Info("IndependentSettingsModule", $"PersistCompleted; Tag='{_selectedSettingsTabTag}'.");
_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 = NormalizeSettingsPageTag(_selectedSettingsTabTag);
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.AppRenderMode = _selectedAppRenderMode;
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();
AppLogger.Info("IndependentSettingsModule", $"PersistScheduled; DelayMs={Math.Max(0, delayMs)}; Tag='{_selectedSettingsTabTag}'.");
_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 NormalizeSettingsPageTag(string? tag)
{
return tag switch
{
null or "" => "General",
_ when string.Equals(tag, "Wallpaper", StringComparison.OrdinalIgnoreCase) => "Appearance",
_ when string.Equals(tag, "Grid", StringComparison.OrdinalIgnoreCase) => "Appearance",
_ when string.Equals(tag, "Color", StringComparison.OrdinalIgnoreCase) => "Appearance",
_ when string.Equals(tag, "StatusBar", StringComparison.OrdinalIgnoreCase) => "Components",
_ when string.Equals(tag, "Region", StringComparison.OrdinalIgnoreCase) => "General",
_ when string.Equals(tag, "Weather", StringComparison.OrdinalIgnoreCase) => "General",
_ when string.Equals(tag, "Launcher", StringComparison.OrdinalIgnoreCase) => "General",
_ => tag
};
}
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;
}
if (radioButton.IsChecked != true)
{
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()
{
return GetSelectedSettingsTabTag() 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 FluentIconVariant variant = FluentIconVariant.Regular;
WallpaperPlacementSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Wallpaper, IconVariant = variant };
ThemeColorSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Color, IconVariant = variant };
StatusBarClockSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Clock, IconVariant = variant };
StatusBarSpacingSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.TextLineSpacing, IconVariant = variant };
WeatherLocationSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.WeatherSunny, IconVariant = variant };
WeatherPreviewSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.WeatherSunny, IconVariant = variant };
WeatherAlertFilterSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Info, IconVariant = variant };
WeatherIconPackSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Color, IconVariant = variant };
WeatherNoTlsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Globe, IconVariant = variant };
LanguageSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Translate, IconVariant = variant };
TimeZoneSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.GlobeClock, IconVariant = variant };
UpdateOptionsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.ArrowClockwiseDashesSettings, IconVariant = variant };
UpdateActionsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.ArrowDownload, IconVariant = variant };
AboutStartupSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Play, IconVariant = variant };
PluginSystemSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.PuzzlePiece, IconVariant = variant };
UpdateThemeModeIcon();
}
private void UpdateThemeModeIcon()
{
ThemeModeSettingsExpander.IconSource = new FluentSymbolIconSource
{
Symbol = _isNightMode ? FluentSymbol.WeatherMoon : FluentSymbol.WeatherSunny,
IconVariant = FluentIconVariant.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

@@ -1,524 +0,0 @@
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)
{
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

@@ -1,317 +0,0 @@
using System;
using System.Linq;
using Avalonia.Controls;
using LanMountainDesktop.Services;
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.shell.title", "Settings");
WindowTitleTextBlock.Text = L("settings.shell.title", "Settings");
WindowSubtitleTextBlock.Text = L("settings.shell.subtitle", "LanMountainDesktop independent settings module");
WindowVersionBadgeTextBlock.Text = GetAppVersionText();
WindowCodeNameBadgeTextBlock.Text = AppCodeName;
SettingsSidebarTitleTextBlock.Text = L("settings.nav_header", "Settings");
SettingsSidebarHintTextBlock.Text = L(
"settings.shell.sidebar_hint",
"Use stable left navigation and a single right-side page host, following the ClassIsland settings rhythm.");
SettingsSidebarFooterTextBlock.Text = L(
"settings.shell.footer_hint",
"Tray-opened settings are managed in this independent settings module.");
InitializeSettingsNavigation();
if (GeneralSettingsHubPanel is not null)
{
GeneralSettingsHubPanel.GeneralPageSubtitleTextBlock.Text = L("settings.page_desc.general", "Manage language, launcher, and weather behavior from the independent settings module.");
GeneralSettingsHubPanel.GeneralRegionSectionTitleTextBlock.Text = L("settings.nav.region", "Region");
GeneralSettingsHubPanel.GeneralRegionSectionHintTextBlock.Text = L("settings.general.region_hint", "Language and time zone settings affect the entire desktop shell.");
GeneralSettingsHubPanel.GeneralLauncherSectionTitleTextBlock.Text = L("settings.nav.launcher", "App Launcher");
GeneralSettingsHubPanel.GeneralLauncherSectionHintTextBlock.Text = L("settings.general.launcher_hint", "Restore hidden entries and adjust how the app launcher behaves.");
GeneralSettingsHubPanel.GeneralWeatherSectionTitleTextBlock.Text = L("settings.nav.weather", "Weather");
GeneralSettingsHubPanel.GeneralWeatherSectionHintTextBlock.Text = L("settings.general.weather_hint", "Configure shared weather source, location, and icon style for weather widgets.");
}
if (AppearanceSettingsHubPanel is not null)
{
AppearanceSettingsHubPanel.AppearancePageSubtitleTextBlock.Text = L("settings.page_desc.appearance", "Personalize wallpaper, desktop grid, and accent colors in one place.");
AppearanceSettingsHubPanel.AppearanceWallpaperSectionTitleTextBlock.Text = L("settings.nav.wallpaper", "Wallpaper");
AppearanceSettingsHubPanel.AppearanceWallpaperSectionHintTextBlock.Text = L("settings.appearance.wallpaper_hint", "Use lightweight thumbnails and asset controls instead of heavy live preview.");
AppearanceSettingsHubPanel.AppearanceGridSectionTitleTextBlock.Text = L("settings.nav.grid", "Grid");
AppearanceSettingsHubPanel.AppearanceGridSectionHintTextBlock.Text = L("settings.appearance.grid_hint", "Tune grid density, spacing, and safe edge inset for the desktop canvas.");
AppearanceSettingsHubPanel.AppearanceColorSectionTitleTextBlock.Text = L("settings.nav.color", "Color");
AppearanceSettingsHubPanel.AppearanceColorSectionHintTextBlock.Text = L("settings.appearance.color_hint", "Choose theme mode and accent colors with Fluent-consistent swatches.");
}
if (ComponentsSettingsHubPanel is not null)
{
ComponentsSettingsHubPanel.ComponentsPageSubtitleTextBlock.Text = L("settings.page_desc.components", "Review available desktop components and configure the status bar area.");
ComponentsSettingsHubPanel.ComponentsSummarySectionTitleTextBlock.Text = L("settings.components.library_title", "Component Library");
ComponentsSettingsHubPanel.ComponentsSummarySectionHintTextBlock.Text = L("settings.components.library_hint", "Built-in and plugin-contributed components available to the desktop editor.");
ComponentsSettingsHubPanel.ComponentsStatusBarSectionTitleTextBlock.Text = L("settings.nav.status_bar", "Status Bar");
ComponentsSettingsHubPanel.ComponentsStatusBarSectionHintTextBlock.Text = L("settings.components.status_bar_hint", "Clock and status-bar component spacing are managed here.");
}
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 (%)");
WeatherPanelTitleTextBlock.Text = L("settings.weather.title", "Weather");
WeatherPreviewSectionTextBlock.Text = L("settings.weather.preview_section", "Weather Preview");
WeatherSettingsSectionTextBlock.Text = L("settings.weather.settings_section", "Settings");
WeatherPreviewButton.Content = L("settings.weather.refresh_button", "Refresh");
WeatherPreviewResultTextBlock.Text = L("settings.weather.preview_hint", "Use refresh to verify your weather configuration.");
WeatherLocationSettingsExpander.Header = L("settings.weather.location_source_header", "Location Source");
WeatherLocationSettingsExpander.Description = L(
"settings.weather.location_source_desc",
"Choose how weather widgets resolve location.");
WeatherLocationModeCityItem.Content = L("settings.weather.mode_city_search", "City Search");
WeatherLocationModeCoordinatesItem.Content = L("settings.weather.mode_coordinates", "Coordinates");
WeatherLocationModeCityChipItem.Content = L("settings.weather.mode_city_search", "City Search");
WeatherLocationModeCoordinatesChipItem.Content = L("settings.weather.mode_coordinates", "Coordinates");
WeatherAutoRefreshToggleSwitch.Content = L("settings.weather.auto_refresh", "Auto refresh location on startup");
WeatherCitySearchSettingsExpander.Header = L("settings.weather.city_search_header", "City Search");
WeatherCitySearchSettingsExpander.Description = L(
"settings.weather.city_search_desc",
"Search cities and apply one weather location.");
WeatherCitySearchTextBox.Watermark = L("settings.weather.search_placeholder", "e.g. Beijing");
WeatherSearchButton.Content = L("settings.weather.search_button", "Search");
WeatherApplyCityButton.Content = L("settings.weather.apply_city_button", "Apply City");
WeatherSearchStatusTextBlock.Text = L("settings.weather.search_hint", "Search by city name and apply one location.");
WeatherCoordinateSettingsExpander.Header = L("settings.weather.coordinates_header", "Coordinates");
WeatherCoordinateSettingsExpander.Description = L(
"settings.weather.coordinates_desc",
"Set latitude/longitude and optional key/name.");
WeatherLatitudeNumberBox.Header = L("settings.weather.latitude_label", "Latitude");
WeatherLongitudeNumberBox.Header = L("settings.weather.longitude_label", "Longitude");
WeatherLocationKeyTextBox.Watermark = L("settings.weather.location_key_placeholder", "Location key (optional)");
WeatherLocationNameTextBox.Watermark = L("settings.weather.location_name_placeholder", "Display name (optional)");
WeatherApplyCoordinatesButton.Content = L("settings.weather.apply_coordinates_button", "Apply Coordinates");
WeatherAlertFilterSettingsExpander.Header = L("settings.weather.alert_filter_header", "Excluded Alerts");
WeatherAlertFilterSettingsExpander.Description = L(
"settings.weather.alert_filter_desc",
"Alerts containing these words will not be shown. One rule per line.");
WeatherAlertListTitleTextBlock.Text = L("settings.weather.alert_list_label", "Exclude List");
WeatherAlertListDescriptionTextBlock.Text = L("settings.weather.alert_list_desc", "One exclusion rule per line.");
WeatherExcludedAlertsTextBox.Watermark = L("settings.weather.alert_filter_placeholder", "One keyword per line");
WeatherNoTlsSettingsExpander.Header = L("settings.weather.no_tls_header", "No TLS Weather Request");
WeatherNoTlsSettingsExpander.Description = L(
"settings.weather.no_tls_desc",
"Not recommended. Enable only for incompatible network environments.");
WeatherNoTlsToggleSwitch.Content = L("settings.weather.no_tls_toggle", "Allow non-TLS request fallback");
WeatherFooterHintTextBlock.Text = L(
"settings.weather.footer_hint",
"Desktop weather widgets will reuse the location and alert exclusion settings configured here.");
WeatherIconPackSettingsExpander.Header = L("settings.weather.icon_style_header", "Weather Icon Style");
WeatherIconPackSettingsExpander.Description = L(
"settings.weather.icon_style_desc",
"Choose Fluent Icon style for weather symbols.");
WeatherIconPackFluentRegularItem.Content = L("settings.weather.icon_style_fluent_regular", "Fluent Regular");
WeatherIconPackFluentFilledItem.Content = L("settings.weather.icon_style_fluent_filled", "Fluent Filled");
UpdateWeatherLocationStatusText();
RegionPanelTitleTextBlock.Text = L("settings.region.title", "Region");
LanguageSettingsExpander.Header = L("settings.region.language_header", "Language");
LanguageSettingsExpander.Description = L("settings.region.language_desc", "Select application language. Changes apply immediately.");
LanguageChineseItem.Content = L("settings.region.language_zh", "Chinese");
LanguageEnglishItem.Content = L("settings.region.language_en", "English");
TimeZoneSettingsExpander.Header = L("settings.region.timezone_header", "Time Zone");
TimeZoneSettingsExpander.Description = L("settings.region.timezone_desc", "Select a time zone. Clock and calendar widgets will follow this zone.");
LauncherSettingsPanelTitleTextBlock.Text = L("settings.launcher.title", "App Launcher");
LauncherHiddenItemsSettingsExpander.Header = L("settings.launcher.hidden_header", "Hidden Items");
LauncherHiddenItemsSettingsExpander.Description = L("settings.launcher.hidden_desc", "Review hidden launcher entries and show them again.");
LauncherHiddenItemsDescriptionTextBlock.Text = L("settings.launcher.hidden_hint", "Right-click an icon in launcher to hide it. Hidden entries appear here.");
LauncherHiddenItemsEmptyTextBlock.Text = L("settings.launcher.hidden_empty", "No hidden items.");
ApplyPluginSettingsLocalization();
ApplyPluginMarketSettingsLocalization();
AboutPanelTitleTextBlock.Text = L("settings.about.title", "About");
VersionTextBlock.Text = Lf("settings.about.version_format", "Version: {0}", GetAppVersionText());
CodeNameTextBlock.Text = Lf("settings.about.codename_format", "Code Name: {0}", AppCodeName);
FontInfoTextBlock.Text = Lf("settings.about.font_format", "Font: {0}", AppFontName);
AboutStartupSettingsExpander.Header = L("settings.about.startup_header", "Windows Startup");
AboutStartupSettingsExpander.Description = L("settings.about.startup_desc", "Launch the app automatically when signing in to Windows.");
AboutRenderModeSettingsExpander.Header = L("settings.about.render_mode_header", "Rendering Mode");
AboutRenderModeSettingsExpander.Description = L(
"settings.about.render_mode_desc",
"Choose the rendering backend. Restart the app after changing this option. Unsupported modes fall back to software.");
SetAppRenderModeComboItemContent(AppRenderingModeHelper.Default, L("settings.about.render_mode.default", "Default"));
SetAppRenderModeComboItemContent(AppRenderingModeHelper.Software, L("settings.about.render_mode.software", "Software"));
SetAppRenderModeComboItemContent(AppRenderingModeHelper.AngleEgl, L("settings.about.render_mode.angle_egl", "angleEgl"));
SetAppRenderModeComboItemContent(AppRenderingModeHelper.Wgl, L("settings.about.render_mode.wgl", "WGL"));
SetAppRenderModeComboItemContent(AppRenderingModeHelper.Vulkan, L("settings.about.render_mode.vulkan", "Vulkan"));
UpdateCurrentRenderBackendStatus();
UpdatePendingRestartDock();
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();
UpdateCurrentSettingsPageHeader(_selectedSettingsTabTag);
}
private void UpdateCurrentSettingsPageHeader(string? tag)
{
if (CurrentSettingsPageTitleTextBlock is null || CurrentSettingsPageSubtitleTextBlock is null)
{
return;
}
var pageTag = string.IsNullOrWhiteSpace(tag) ? "General" : NormalizeSettingsPageTag(tag);
CurrentSettingsPageTitleTextBlock.Text = _settingsPageDefinitions.TryGetValue(pageTag, out var definition)
? definition.Title
: L("settings.shell.title", "Settings");
CurrentSettingsPageSubtitleTextBlock.Text = GetSettingsPageDescription(pageTag);
}
private string GetSettingsPageDescription(string tag)
{
if (_settingsPageDefinitions.TryGetValue(tag, out var definition))
{
return definition.Description;
}
return L("settings.shell.sidebar_hint", "Use stable left navigation and a single right-side page host, following the ClassIsland settings rhythm.");
}
private void SetAppRenderModeComboItemContent(string tag, string content)
{
var item = AppRenderModeComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(candidate =>
string.Equals(candidate.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase));
if (item is not null)
{
item.Content = content;
}
}
private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone)
{
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
var sign = offset >= TimeSpan.Zero ? "+" : "-";
var hours = Math.Abs(offset.Hours);
var minutes = Math.Abs(offset.Minutes);
var name = string.IsNullOrWhiteSpace(timeZone.StandardName) ? timeZone.DisplayName : timeZone.StandardName;
if (string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) &&
ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName))
{
name = localizedName;
}
if (string.IsNullOrWhiteSpace(name))
{
name = timeZone.Id;
}
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {name}";
}
private static string GetAppVersionText()
{
var version = typeof(MainWindow).Assembly.GetName().Version;
if (version is null || version.Major < 0 || version.Minor < 0 || version.Build < 0)
{
return FallbackAppVersion;
}
return $"{version.Major}.{version.Minor}.{version.Build}";
}
private void OnLanguageSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressLanguageSelectionEvents || LanguageComboBox.SelectedItem is not ComboBoxItem item)
{
return;
}
_languageCode = _localizationService.NormalizeLanguageCode(item.Tag as string);
ApplyLocalization();
ThemeColorStatusTextBlock.Text = Lf("settings.region.applied_format", "Language switched to: {0}", GetLanguageDisplayName(_languageCode));
PersistSettings();
}
}

View File

@@ -1,42 +0,0 @@
using System;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void UpdateCurrentRenderBackendStatus()
{
var backendInfo = AppRenderBackendDiagnostics.Detect();
var localizedBackend = GetLocalizedRenderBackendName(backendInfo.ActualBackend);
CurrentRenderBackendLabelTextBlock.Text = L(
"settings.about.render_mode.current_label",
"Current actual backend");
CurrentRenderBackendValueTextBlock.Text = Lf(
"settings.about.render_mode.current_format",
"Current backend: {0}",
localizedBackend);
CurrentRenderBackendImplementationTextBlock.Text = string.IsNullOrWhiteSpace(backendInfo.ImplementationTypeName)
? L(
"settings.about.render_mode.impl_unavailable",
"Runtime implementation is unavailable.")
: Lf(
"settings.about.render_mode.impl_format",
"Runtime implementation: {0}",
backendInfo.ImplementationTypeName);
}
private string GetLocalizedRenderBackendName(string renderBackend)
{
return renderBackend switch
{
AppRenderingModeHelper.Default => L("settings.about.render_mode.default", "Default"),
AppRenderingModeHelper.Software => L("settings.about.render_mode.software", "Software"),
AppRenderingModeHelper.AngleEgl => L("settings.about.render_mode.angle_egl", "angleEgl"),
AppRenderingModeHelper.Wgl => L("settings.about.render_mode.wgl", "WGL"),
AppRenderingModeHelper.Vulkan => L("settings.about.render_mode.vulkan", "Vulkan"),
_ => L("settings.about.render_mode.unknown", "Unknown")
};
}
}

View File

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

View File

@@ -1,76 +0,0 @@
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

@@ -1,339 +0,0 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class 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,
Verb = "runas"
});
_updateStatusText = L("settings.update.status_installer_started", "Installer started. The app will close for update.");
UpdateUpdatePanelState();
_ = App.CurrentHostApplicationLifecycle?.TryExit(new HostApplicationLifecycleRequest(
Source: nameof(SettingsWindow),
Reason: "Update installer started successfully from settings."));
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
_updateStatusText = L(
"settings.update.status_elevation_cancelled",
"Administrator permission was not granted. Update was cancelled.");
UpdateUpdatePanelState();
}
catch (Exception ex)
{
_updateStatusText = Lf("settings.update.status_launch_failed_format", "Failed to start installer: {0}", ex.Message);
UpdateUpdatePanelState();
}
}
private void UpdateUpdatePanelState()
{
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;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,360 +0,0 @@
<local:IndependentSettingsModuleWindowBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:comp="using:LanMountainDesktop.Views.Components"
xmlns:local="using:LanMountainDesktop.Views"
xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia"
x:Class="LanMountainDesktop.Views.SettingsWindow"
x:Name="IndependentSettingsModuleWindow"
Title="Settings"
Icon="/Assets/avalonia-logo.ico"
Width="1240"
Height="860"
MinWidth="980"
MinHeight="680"
ShowInTaskbar="True"
WindowStartupLocation="CenterScreen"
SystemDecorations="None"
CanResize="True"
UseLayoutRounding="True"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
windowing:AppWindow.AllowInteractionInTitleBar="True"
FontFamily="Segoe UI Variable Text, {DynamicResource AppFontFamily}">
<Window.Styles>
<Style Selector="Border.independent-settings-shell">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="BoxShadow" Value="0 12 36 #22000000" />
</Style>
<Style Selector="TextBlock.independent-settings-hint">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
<Setter Property="FontSize" Value="12.5" />
</Style>
<Style Selector="Border.independent-settings-titlebar">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorSecondaryBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style>
<Style Selector="Button.independent-settings-titlebar-button">
<Setter Property="Width" Value="46" />
<Setter Property="Height" Value="32" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
</Style>
<Style Selector="Button.independent-settings-titlebar-button:pointerover">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Style>
<Style Selector="Button.independent-settings-titlebar-close:pointerover">
<Setter Property="Background" Value="#CCB91C1C" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.independent-settings-pane-button">
<Setter Property="Width" Value="40" />
<Setter Property="Height" Value="40" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="Button.independent-settings-pane-button:pointerover">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Style>
<Style Selector="ui|NavigationView#SettingsNavView">
<Setter Property="PaneDisplayMode" Value="Auto" />
<Setter Property="IsBackButtonVisible" Value="False" />
<Setter Property="IsPaneToggleButtonVisible" Value="False" />
<Setter Property="IsSettingsVisible" Value="False" />
<Setter Property="OpenPaneLength" Value="283" />
<Setter Property="CompactPaneLength" Value="0" />
<Setter Property="AlwaysShowHeader" Value="False" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="ui|NavigationViewItem">
<Setter Property="Margin" Value="6,2" />
</Style>
<Style Selector="ui|NavigationViewItemHeader">
<Setter Property="Margin" Value="14,14,0,4" />
</Style>
<Style Selector="ui|InfoBar#IndependentSettingsStatusInfoBar">
<Setter Property="Margin" Value="0,0,0,14" />
</Style>
<Style Selector="Border.settings-page-shell">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseAltBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="Padding" Value="24" />
</Style>
</Window.Styles>
<Grid x:Name="DesktopHost"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}">
<Border x:Name="DesktopWallpaperLayer"
IsVisible="False"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Border Margin="12"
Classes="independent-settings-shell">
<Grid RowDefinitions="48,*">
<Border Grid.Row="0"
Classes="independent-settings-titlebar"
PointerPressed="OnTitleBarPointerPressed"
DoubleTapped="OnTitleBarDoubleTapped">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto"
ColumnSpacing="10"
Margin="10,0,8,0">
<Button x:Name="SettingsPaneToggleButton"
Grid.Column="0"
Classes="independent-settings-pane-button"
Click="OnSettingsPaneToggleButtonClick"
ToolTip.Tip="Toggle navigation">
<fi:SymbolIcon Symbol="Navigation" />
</Button>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="10"
VerticalAlignment="Center">
<Border Width="28"
Height="28"
CornerRadius="9"
Background="{DynamicResource AccentFillColorDefaultBrush}">
<TextBlock Text="L"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Spacing="0"
VerticalAlignment="Center">
<TextBlock x:Name="WindowTitleTextBlock"
FontSize="13"
FontWeight="SemiBold"
Text="Settings" />
<TextBlock x:Name="WindowSubtitleTextBlock"
Classes="independent-settings-hint"
Text="LanMountainDesktop independent settings module" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="3"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center"
HorizontalAlignment="Right">
<Border Classes="settings-page-shell"
Padding="10,4"
CornerRadius="12">
<TextBlock x:Name="WindowVersionBadgeTextBlock"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
FontWeight="SemiBold"
Text="1.0.0" />
</Border>
<Border Classes="settings-page-shell"
Padding="10,4"
CornerRadius="12">
<TextBlock x:Name="WindowCodeNameBadgeTextBlock"
Classes="independent-settings-hint"
Text="Administrate" />
</Border>
<Button Classes="independent-settings-pane-button"
ToolTip.Tip="More options">
<fi:SymbolIcon Symbol="MoreHorizontal" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Header="Open Logs Folder"
Click="OnOpenLogsFolderClick">
<MenuItem.Icon>
<fi:SymbolIcon Symbol="FolderOpen" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Open App Folder"
Click="OnOpenAppFolderClick">
<MenuItem.Icon>
<fi:SymbolIcon Symbol="FolderOpen" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
<StackPanel Grid.Column="4"
Orientation="Horizontal"
Spacing="4"
VerticalAlignment="Center">
<Button Classes="independent-settings-titlebar-button"
Click="OnMinimizeWindowClick">
<fi:SymbolIcon Symbol="Subtract" />
</Button>
<Button Classes="independent-settings-titlebar-button"
Click="OnToggleWindowStateClick">
<fi:SymbolIcon x:Name="WindowStateToggleIcon"
Symbol="Square" />
</Button>
<Button Classes="independent-settings-titlebar-button independent-settings-titlebar-close"
Click="OnCloseWindowClick">
<fi:SymbolIcon Symbol="Dismiss" />
</Button>
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="1">
<ui:NavigationView x:Name="SettingsNavView"
SelectionChanged="OnSettingsNavSelectionChanged">
<ui:NavigationView.PaneHeader>
<StackPanel Margin="10,10,10,12"
Spacing="4">
<TextBlock x:Name="SettingsSidebarTitleTextBlock"
FontSize="15"
FontWeight="SemiBold"
Text="Settings" />
<TextBlock x:Name="SettingsSidebarHintTextBlock"
Classes="independent-settings-hint"
TextWrapping="Wrap"
Text="Use stable left navigation and a single right-side page host, following the ClassIsland settings rhythm." />
</StackPanel>
</ui:NavigationView.PaneHeader>
<ui:NavigationView.PaneFooter>
<Border Margin="10,10,10,12"
Classes="settings-page-shell"
Padding="12">
<TextBlock x:Name="SettingsSidebarFooterTextBlock"
Classes="independent-settings-hint"
TextWrapping="Wrap"
Text="Both the tray entry and the in-app settings entry now open this same independent settings module." />
</Border>
</ui:NavigationView.PaneFooter>
<Grid RowDefinitions="Auto,*">
<ui:InfoBar x:Name="IndependentSettingsStatusInfoBar"
Title="Independent settings module status"
IsClosable="True"
IsOpen="False"
Severity="Warning" />
<Grid Grid.Row="1"
Margin="18,4,18,18"
RowDefinitions="Auto,*,Auto">
<StackPanel Spacing="4"
Margin="4,0,0,16"
MaxWidth="980"
HorizontalAlignment="Left">
<TextBlock x:Name="CurrentSettingsPageTitleTextBlock"
FontSize="28"
FontWeight="SemiBold"
Text="General" />
<TextBlock x:Name="CurrentSettingsPageSubtitleTextBlock"
Classes="independent-settings-hint"
MaxWidth="720"
TextWrapping="Wrap"
Text="Configure this part of LanMountainDesktop in the independent settings module." />
</StackPanel>
<Border Grid.Row="1"
Classes="settings-page-shell"
MaxWidth="980"
HorizontalAlignment="Left">
<ScrollViewer x:Name="SettingsContentScrollViewer"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ui:Frame x:Name="SettingsPageFrame"
Margin="0" />
</ScrollViewer>
</Border>
<Border x:Name="PendingRestartDock"
Grid.Row="2"
IsVisible="False"
Margin="0,14,0,0"
Classes="settings-page-shell"
Padding="14,12"
MaxWidth="980"
HorizontalAlignment="Left">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="12">
<Border Width="36"
Height="36"
CornerRadius="18"
Background="{DynamicResource AccentFillColorDefaultBrush}">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular"
FontSize="16"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="PendingRestartDockTitleTextBlock"
FontSize="14"
FontWeight="SemiBold"
Text="Restart required" />
<TextBlock x:Name="PendingRestartDockDescriptionTextBlock"
Classes="independent-settings-hint"
TextWrapping="Wrap"
Text="Your changes will apply after restarting the app." />
</StackPanel>
<Button x:Name="PendingRestartDockButton"
Grid.Column="2"
Padding="14,8"
Click="OnPendingRestartDockButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular" />
<TextBlock x:Name="PendingRestartDockButtonTextBlock"
VerticalAlignment="Center"
Text="Restart app" />
</StackPanel>
</Button>
</Grid>
</Border>
</Grid>
</Grid>
</ui:NavigationView>
</Grid>
</Grid>
</Border>
<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>
</local:IndependentSettingsModuleWindowBase>

View File

@@ -1,606 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
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 LanMountainDesktop.Views.SettingsPages;
using LibVLCSharp.Shared;
using Line = Avalonia.Controls.Shapes.Line;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow : IndependentSettingsModuleWindowBase
{
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 readonly Dictionary<string, NavigationViewItem> _settingsNavItems = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, NavigationViewItem> _pluginSettingsNavItems = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, IndependentSettingsPageDefinition> _settingsPageDefinitions = new(StringComparer.OrdinalIgnoreCase);
private GeneralSettingsPage? GeneralSettingsHubPanel;
private AppearanceSettingsPage? AppearanceSettingsHubPanel;
private ComponentsSettingsPage? ComponentsSettingsHubPanel;
private WallpaperSettingsPage? WallpaperSettingsPanel;
private GridSettingsPage? GridSettingsPanel;
private ColorSettingsPage? ColorSettingsPanel;
private StatusBarSettingsPage? StatusBarSettingsPanel;
private WeatherSettingsPage? WeatherSettingsPanel;
private RegionSettingsPage? RegionSettingsPanel;
private UpdateSettingsPage? UpdateSettingsPanel;
private LauncherSettingsPage? LauncherSettingsPanel;
private AboutSettingsPage? AboutSettingsPanel;
private PluginSettingsPage? PluginSettingsPanel;
private PluginMarketSettingsPage? PluginMarketSettingsPanel;
private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
private byte[]? _launcherFolderIconPngBytes;
private Bitmap? _launcherFolderIconBitmap;
private int _targetShortSideCells;
private bool _isNightMode;
private bool _enableDynamicTaskbarActions;
private bool _suppressThemeToggleEvents;
private bool _suppressLanguageSelectionEvents;
private bool _suppressTimeZoneSelectionEvents;
private bool _suppressWeatherLocationEvents;
private bool _suppressSettingsPersistence;
private bool _suppressGridInsetEvents;
private bool _suppressStatusBarSpacingEvents;
private bool _suppressAutoStartToggleEvents;
private bool _suppressAppRenderModeSelectionEvents;
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 readonly object _previewVideoFrameSync = new();
private MediaPlayer.LibVLCVideoLockCb? _previewVideoLockCallback;
private MediaPlayer.LibVLCVideoUnlockCb? _previewVideoUnlockCallback;
private MediaPlayer.LibVLCVideoDisplayCb? _previewVideoDisplayCallback;
private DispatcherTimer? _previewVideoFrameRefreshTimer;
private IntPtr _previewVideoFrameBufferPtr;
private byte[]? _previewVideoStagingBuffer;
private WriteableBitmap? _previewVideoBitmap;
private int _previewVideoFrameWidth;
private int _previewVideoFrameHeight;
private int _previewVideoFramePitch;
private int _previewVideoFrameBufferSize;
private int _previewVideoFrameDirtyFlag;
private bool _previewVideoSnapshotPending;
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 string _gridSpacingPreset = "Relaxed";
private string _statusBarSpacingMode = "Relaxed";
private int _statusBarCustomSpacingPercent = 12;
private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent;
private string _selectedAppRenderMode = AppRenderingModeHelper.Default;
private string _runningAppRenderMode = AppRenderingModeHelper.Default;
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 string _selectedSettingsTabTag = "General";
private WallpaperPlacement _selectedWallpaperPlacement = WallpaperPlacement.Fill;
private bool _isWeatherSearchInProgress;
private bool _isWeatherPreviewInProgress;
private bool _controlsBound;
private bool _independentModuleInitializationCompleted;
private bool _suppressWallpaperPlacementEvents;
private bool _isIndependentSettingsModuleClosing;
private bool _allowIndependentSettingsModuleRealClose;
public SettingsWindow()
{
_componentRegistry = DesktopComponentRegistryFactory.Create((Application.Current as App)?.PluginRuntimeService);
InitializeComponent();
InitializeSettingsPageHosts();
InitializeSettingsNavigation();
InitializePluginSettingsNavigation();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
RequestedThemeVariant = Application.Current?.RequestedThemeVariant ?? ThemeVariant.Default;
PendingRestartStateService.StateChanged += OnPendingRestartStateChanged;
Closing += OnIndependentSettingsModuleClosing;
Opened += OnWindowOpened;
}
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.IsCheckedChanged += OnNightModeIsCheckedChanged;
RecommendedColorButton1.Click += OnRecommendedColorClick;
RecommendedColorButton2.Click += OnRecommendedColorClick;
RecommendedColorButton3.Click += OnRecommendedColorClick;
RecommendedColorButton4.Click += OnRecommendedColorClick;
RecommendedColorButton5.Click += OnRecommendedColorClick;
RecommendedColorButton6.Click += OnRecommendedColorClick;
RefreshMonetColorsButton.Click += OnRefreshMonetColorsClick;
MonetColorButton1.Click += OnMonetColorClick;
MonetColorButton2.Click += OnMonetColorClick;
MonetColorButton3.Click += OnMonetColorClick;
MonetColorButton4.Click += OnMonetColorClick;
MonetColorButton5.Click += OnMonetColorClick;
MonetColorButton6.Click += OnMonetColorClick;
StatusBarClockToggleSwitch.IsCheckedChanged += OnStatusBarClockIsCheckedChanged;
ClockFormatHMSSRadio.IsCheckedChanged += OnClockFormatChanged;
ClockFormatHMRadio.IsCheckedChanged += OnClockFormatChanged;
StatusBarSpacingModeComboBox.SelectionChanged += OnStatusBarSpacingModeChanged;
StatusBarSpacingSlider.ValueChanged += OnStatusBarSpacingSliderChanged;
WeatherPreviewButton.Click += OnTestWeatherRequestClick;
WeatherLocationModeComboBox.SelectionChanged += OnWeatherLocationModeSelectionChanged;
WeatherLocationModeChipListBox.SelectionChanged += OnWeatherLocationModeChipSelectionChanged;
WeatherAutoRefreshToggleSwitch.IsCheckedChanged += OnWeatherAutoRefreshToggled;
WeatherSearchButton.Click += OnSearchWeatherCityClick;
WeatherApplyCityButton.Click += OnApplyWeatherCitySelectionClick;
WeatherApplyCoordinatesButton.Click += OnApplyWeatherCoordinatesClick;
WeatherExcludedAlertsTextBox.LostFocus += OnWeatherExcludedAlertsLostFocus;
WeatherIconPackComboBox.SelectionChanged += OnWeatherIconPackSelectionChanged;
WeatherNoTlsToggleSwitch.IsCheckedChanged += OnWeatherNoTlsToggled;
LanguageComboBox.SelectionChanged += OnLanguageSelectionChanged;
TimeZoneComboBox.SelectionChanged += OnTimeZoneSelectionChanged;
AutoCheckUpdatesToggleSwitch.IsCheckedChanged += OnAutoCheckUpdatesToggled;
UpdateChannelChipListBox.SelectionChanged += OnUpdateChannelSelectionChanged;
CheckForUpdatesButton.Click += OnCheckForUpdatesClick;
DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick;
AutoStartWithWindowsToggleSwitch.IsCheckedChanged += OnAutoStartWithWindowsToggled;
AppRenderModeComboBox.SelectionChanged += OnAppRenderModeSelectionChanged;
}
private void EnsureIndependentModuleControlsBound()
{
if (_controlsBound)
{
return;
}
AppLogger.Info("IndependentSettingsModule", "ControlsBindingStarted.");
try
{
HookEvents();
_controlsBound = true;
AppLogger.Info("IndependentSettingsModule", "ControlsBindingCompleted.");
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
AppLogger.Warn("IndependentSettingsModule", "ControlsBindingFailed.", ex);
throw new InvalidOperationException("Failed to bind independent settings module controls.", ex);
}
}
private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
{
if (sender is not ToggleButton toggleButton)
{
return;
}
if (toggleButton.IsChecked == true)
{
OnNightModeChecked(sender, e);
return;
}
OnNightModeUnchecked(sender, e);
}
private void OnStatusBarClockIsCheckedChanged(object? sender, RoutedEventArgs e)
{
if (sender is not ToggleButton toggleButton)
{
return;
}
if (toggleButton.IsChecked == true)
{
OnStatusBarClockChecked(sender, e);
return;
}
OnStatusBarClockUnchecked(sender, e);
}
private void OnWindowOpened(object? sender, EventArgs e)
{
Opened -= OnWindowOpened;
UpdateWindowChromeState();
UiExceptionGuard.FireAndForgetGuarded(
async () =>
{
EnsureIndependentModuleControlsBound();
await InitializeIndependentSettingsModuleAsync();
},
"IndependentSettingsModule.Initialize",
UiExceptionGuard.BuildContext(("Window", nameof(SettingsWindow))),
ex =>
{
ShowIndependentModuleStatus(
L("settings.shell.init_failed_title", "设置模块初始化失败"),
ex.Message,
InfoBarSeverity.Warning);
return Task.CompletedTask;
});
}
private void OnIndependentSettingsModuleClosing(object? sender, WindowClosingEventArgs e)
{
AppLogger.Info(
"IndependentSettingsModule",
$"CloseRequested; AllowRealClose={_allowIndependentSettingsModuleRealClose}; Reason='{e.CloseReason}'.");
if (!_allowIndependentSettingsModuleRealClose &&
e.CloseReason is not WindowCloseReason.ApplicationShutdown &&
e.CloseReason is not WindowCloseReason.OSShutdown)
{
e.Cancel = true;
PersistSettings();
Hide();
AppLogger.Info("IndependentSettingsModule", "WindowHiddenByClose.");
return;
}
_isIndependentSettingsModuleClosing = true;
}
private async Task InitializeIndependentSettingsModuleAsync()
{
if (_independentModuleInitializationCompleted)
{
return;
}
AppLogger.Info("IndependentSettingsModule", "ModuleInitStarted; Stage='Opened'.");
_suppressSettingsPersistence = true;
try
{
ShowIndependentModuleStatus(string.Empty, string.Empty, InfoBarSeverity.Informational, isOpen: false);
var snapshot = new AppSettingsSnapshot();
var launcherSnapshot = new LauncherSettingsSnapshot();
await RunInitializationStageAsync("SnapshotLoad", () =>
{
snapshot = _appSettingsService.Load();
launcherSnapshot = _launcherSettingsService.Load();
return Task.CompletedTask;
});
await RunInitializationStageAsync("BaseConfiguration", () =>
{
_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);
InitializeAppRenderModeSetting(snapshot);
InitializeUpdateSettings(snapshot);
InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeSettingsIcons();
ApplyLocalization();
return Task.CompletedTask;
});
await RunInitializationStageAsync("VisualState", () =>
{
_selectedWallpaperPlacement = GetWallpaperPlacementFromIndex(GetPlacementIndexFromSetting(snapshot.WallpaperPlacement));
_suppressWallpaperPlacementEvents = true;
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
_suppressWallpaperPlacementEvents = false;
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);
_defaultDesktopBackground = DesktopWallpaperLayer.Background;
RestoreSettingsTabSelection(snapshot);
UpdateSettingsTabContent();
UpdateWallpaperDisplay();
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
InitializeTimeZoneSettings();
return Task.CompletedTask;
});
UiExceptionGuard.FireAndForgetGuarded(
LoadLauncherEntriesAsync,
"IndependentSettingsModule.LoadLauncherEntries",
UiExceptionGuard.BuildContext(("Window", nameof(SettingsWindow))),
ex =>
{
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能载入"),
ex.Message,
InfoBarSeverity.Warning);
return Task.CompletedTask;
});
_independentModuleInitializationCompleted = true;
AppLogger.Info("IndependentSettingsModule", "ModuleInitCompleted.");
}
finally
{
_suppressSettingsPersistence = false;
}
}
private async Task RunInitializationStageAsync(string stage, Func<Task> action)
{
AppLogger.Info("IndependentSettingsModule", $"ModuleInitStarted; Stage='{stage}'.");
try
{
await action();
AppLogger.Info("IndependentSettingsModule", $"ModuleInitCompleted; Stage='{stage}'.");
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
AppLogger.Warn("IndependentSettingsModule", $"ModuleInitFailed; Stage='{stage}'.", ex);
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能载入"),
ex.Message,
InfoBarSeverity.Warning);
}
}
private void ShowIndependentModuleStatus(string title, string message, InfoBarSeverity severity, bool isOpen = true)
{
if (IndependentSettingsStatusInfoBar is null)
{
return;
}
IndependentSettingsStatusInfoBar.Title = title;
IndependentSettingsStatusInfoBar.Message = message;
IndependentSettingsStatusInfoBar.Severity = severity;
IndependentSettingsStatusInfoBar.IsOpen = isOpen;
}
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
BeginMoveDrag(e);
}
}
private void OnTitleBarDoubleTapped(object? sender, RoutedEventArgs e)
{
if (!CanResize)
{
return;
}
WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
UpdateWindowChromeState();
}
private void OnMinimizeWindowClick(object? sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
UpdateWindowChromeState();
}
private void OnToggleWindowStateClick(object? sender, RoutedEventArgs e)
{
if (!CanResize)
{
return;
}
WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
UpdateWindowChromeState();
}
private void UpdateWindowChromeState()
{
if (WindowStateToggleIcon is null)
{
return;
}
WindowStateToggleIcon.Symbol = WindowState == WindowState.Maximized
? FluentIcons.Common.Symbol.SquareMultiple
: FluentIcons.Common.Symbol.Square;
}
private void OnCloseWindowClick(object? sender, RoutedEventArgs e)
{
Close();
}
private void OnSettingsPaneToggleButtonClick(object? sender, RoutedEventArgs e)
{
if (SettingsNavView is not null)
{
SettingsNavView.IsPaneOpen = !SettingsNavView.IsPaneOpen;
}
}
private void OnOpenLogsFolderClick(object? sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = AppLogger.LogDirectory,
UseShellExecute = true
});
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能加载"),
ex.Message,
InfoBarSeverity.Warning);
}
}
private void OnOpenAppFolderClick(object? sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = Path.GetFullPath(".") ?? string.Empty,
UseShellExecute = true
});
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能加载"),
ex.Message,
InfoBarSeverity.Warning);
}
}
}

View File

@@ -21,7 +21,7 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
IPlugin plugin,
IPluginRuntimeContext runtimeContext,
IServiceProvider services,
IReadOnlyList<PluginSettingsPageRegistration> settingsPages,
IReadOnlyList<PluginSettingsSectionRegistration> settingsSections,
IReadOnlyList<PluginDesktopComponentRegistration> desktopComponents,
IReadOnlyList<PluginServiceExportDescriptor> exportedServices,
IReadOnlyList<IHostedService> hostedServices,
@@ -34,7 +34,7 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
Plugin = plugin;
RuntimeContext = runtimeContext;
Services = services;
SettingsPages = settingsPages;
SettingsSections = settingsSections;
DesktopComponents = desktopComponents;
ExportedServices = exportedServices;
HostedServices = hostedServices;
@@ -57,7 +57,7 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
public IServiceProvider Services { get; }
public IReadOnlyList<PluginSettingsPageRegistration> SettingsPages { get; }
public IReadOnlyList<PluginSettingsSectionRegistration> SettingsSections { get; }
public IReadOnlyList<PluginDesktopComponentRegistration> DesktopComponents { get; }

View File

@@ -1,166 +1,26 @@
using System;
using System.Collections.Generic;
using System;
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(SettingsNavPluginMarketItem) + 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(contribution.Plugin.Services);
}
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
}
};
// Legacy plugin settings pages are removed in API-only settings mode.
}
private void UpdatePluginSettingsPageVisibility(string? selectedTag)
{
foreach (var pair in _pluginSettingsPageHosts)
{
pair.Value.IsVisible = string.Equals(pair.Key, selectedTag, StringComparison.OrdinalIgnoreCase);
}
_ = selectedTag;
}
internal void RefreshPluginSettingsNavigation()
{
if (SettingsNavView?.MenuItems is null)
{
return;
}
foreach (var pair in _pluginSettingsPageHosts.ToArray())
{
var navItem = SettingsNavView.MenuItems
.OfType<NavigationViewItem>()
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), pair.Key, StringComparison.OrdinalIgnoreCase));
if (navItem is not null)
{
SettingsNavView.MenuItems.Remove(navItem);
}
SettingsContentPagesHost.Children.Remove(pair.Value);
}
_pluginSettingsPageHosts.Clear();
InitializePluginSettingsNavigation();
// Legacy plugin settings pages are removed in API-only settings mode.
}
private string? GetSelectedSettingsTabTag()
@@ -212,6 +72,3 @@ public partial class MainWindow
}
}
}

View File

@@ -3,9 +3,9 @@ using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
public sealed record PluginSettingsPageContribution(
public sealed record PluginSettingsSectionContribution(
LoadedPlugin Plugin,
PluginSettingsPageRegistration Registration);
PluginSettingsSectionRegistration Registration);
public sealed record PluginDesktopComponentContribution(
LoadedPlugin Plugin,

View File

@@ -170,10 +170,10 @@ public sealed class PluginLoader
AppLogger.Info("PluginLoader", $"Service provider built. PluginId='{manifest.Id}'.");
runtimeContext.SetServices(pluginServices);
var settingsPages = pluginServices
.GetServices<PluginSettingsPageRegistration>()
.OrderBy(page => page.SortOrder)
.ThenBy(page => page.Title, StringComparer.OrdinalIgnoreCase)
var settingsSections = pluginServices
.GetServices<PluginSettingsSectionRegistration>()
.OrderBy(section => section.SortOrder)
.ThenBy(section => section.TitleLocalizationKey, StringComparer.OrdinalIgnoreCase)
.ToArray();
var desktopComponents = pluginServices
.GetServices<PluginDesktopComponentRegistration>()
@@ -183,7 +183,7 @@ public sealed class PluginLoader
var exportedServices = ResolveExports(manifest, pluginServices);
AppLogger.Info(
"PluginLoader",
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsPages={settingsPages.Length}; Widgets={desktopComponents.Length}; Exports={exportedServices.Count}.");
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsSections={settingsSections.Length}; Widgets={desktopComponents.Length}; Exports={exportedServices.Count}.");
hostedServices = pluginServices.GetServices<IHostedService>().ToArray();
StartHostedServices(hostedServices);
AppLogger.Info("PluginLoader", $"Hosted services started. PluginId='{manifest.Id}'; HostedServices={hostedServices.Count}.");
@@ -196,7 +196,7 @@ public sealed class PluginLoader
plugin,
runtimeContext,
pluginServices,
settingsPages,
settingsSections,
desktopComponents,
exportedServices,
hostedServices,
@@ -314,6 +314,8 @@ public sealed class PluginLoader
RegisterHostService<IPluginPackageManager>(services, hostServices);
RegisterHostService<IHostApplicationLifecycle>(services, hostServices);
RegisterHostService<IPluginExportRegistry>(services, hostServices);
RegisterHostService<ISettingsService>(services, hostServices);
RegisterHostService<ISettingsCatalog>(services, hostServices);
return services;
}

View File

@@ -1,7 +1,7 @@
using System;
using System.IO;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketCacheService
{

View File

@@ -16,7 +16,7 @@ using Avalonia.Media.Imaging;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class PluginMarketEmbeddedView : UserControl, IDisposable
{

View File

@@ -5,7 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketIconService : IDisposable
{

View File

@@ -5,7 +5,7 @@ using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketIndexService : IDisposable
{

View File

@@ -8,7 +8,7 @@ using System.Security.Cryptography;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketInstallService : IDisposable
{

View File

@@ -6,7 +6,7 @@ using System.Linq;
using System.Text.Json;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal static class AirAppMarketDefaults
{

View File

@@ -4,7 +4,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketReadmeService : IDisposable
{

View File

@@ -5,7 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketReleaseResolverService
{

View File

@@ -2,6 +2,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Views.SettingsPages;

View File

@@ -12,6 +12,7 @@ using Avalonia.Markup.Xaml;
using LanMountainDesktop.Models;
using LanMountainDesktop.Plugins;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -29,10 +30,12 @@ public sealed class PluginRuntimeService : IDisposable
private readonly PluginSharedContractManager _sharedContractManager;
private readonly IServiceProvider _hostServices;
private readonly IPluginPackageManager _packageManager;
private readonly SettingsFacadeService _settingsFacade;
private readonly SettingsCatalogService _settingsCatalogService;
private readonly List<LoadedPlugin> _loadedPlugins = [];
private readonly List<PluginLoadResult> _loadResults = [];
private readonly List<PluginCatalogEntry> _catalog = [];
private readonly List<PluginSettingsPageContribution> _settingsPages = [];
private readonly List<PluginSettingsSectionContribution> _settingsSections = [];
private readonly List<PluginDesktopComponentContribution> _desktopComponents = [];
private readonly object _packageMutationGate = new();
@@ -42,7 +45,14 @@ public sealed class PluginRuntimeService : IDisposable
_sharedContractManager = new PluginSharedContractManager(
Path.Combine(GetUserDataRootDirectory(), "PluginMarket"));
_packageManager = new PluginRuntimePackageManager(this);
_hostServices = new PluginHostServiceProvider(_packageManager, _applicationLifecycle, _exportRegistry);
_settingsFacade = new SettingsFacadeService(this);
_settingsCatalogService = (SettingsCatalogService)_settingsFacade.Catalog;
_hostServices = new PluginHostServiceProvider(
_packageManager,
_applicationLifecycle,
_exportRegistry,
_settingsFacade.Settings,
_settingsFacade.Catalog);
_loaderOptions = CreateOptions();
_loader = new PluginLoader(_loaderOptions);
}
@@ -55,12 +65,14 @@ public sealed class PluginRuntimeService : IDisposable
public IReadOnlyList<PluginCatalogEntry> Catalog => _catalog;
public IReadOnlyList<PluginSettingsPageContribution> SettingsPages => _settingsPages;
public IReadOnlyList<PluginSettingsSectionContribution> SettingsSections => _settingsSections;
public IReadOnlyList<PluginDesktopComponentContribution> DesktopComponents => _desktopComponents;
public IPluginExportRegistry ExportRegistry => _exportRegistry;
public ISettingsFacadeService SettingsFacade => _settingsFacade;
public void LoadInstalledPlugins()
{
Directory.CreateDirectory(PluginsDirectory);
@@ -172,11 +184,11 @@ public sealed class PluginRuntimeService : IDisposable
true,
true,
null,
loadResult.LoadedPlugin.SettingsPages.Count,
loadResult.LoadedPlugin.SettingsSections.Count,
loadResult.LoadedPlugin.DesktopComponents.Count));
AppLogger.Info(
"PluginRuntime",
$"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? "<unknown>"}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? "<unknown>"}'; SourceKind='{candidate.SourceKind}'; SettingsPages={loadResult.LoadedPlugin.SettingsPages.Count}; Widgets={loadResult.LoadedPlugin.DesktopComponents.Count}.");
$"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? "<unknown>"}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? "<unknown>"}'; SourceKind='{candidate.SourceKind}'; SettingsSections={loadResult.LoadedPlugin.SettingsSections.Count}; Widgets={loadResult.LoadedPlugin.DesktopComponents.Count}.");
Debug.WriteLine($"[PluginRuntime] Loaded '{loadResult.Manifest?.Id}' from '{loadResult.SourcePath}'.");
continue;
}
@@ -374,13 +386,16 @@ public sealed class PluginRuntimeService : IDisposable
{
UnloadInstalledPlugins();
_sharedContractManager.Dispose();
_settingsFacade.Dispose();
}
private void UnloadInstalledPlugins()
{
for (var i = _loadedPlugins.Count - 1; i >= 0; i--)
{
_exportRegistry.RemoveExports(_loadedPlugins[i].Manifest.Id);
var pluginId = _loadedPlugins[i].Manifest.Id;
_exportRegistry.RemoveExports(pluginId);
_settingsCatalogService.RemovePluginSections(pluginId);
_loadedPlugins[i].Dispose();
}
@@ -388,7 +403,7 @@ public sealed class PluginRuntimeService : IDisposable
_exportRegistry.Clear();
_loadResults.Clear();
_catalog.Clear();
_settingsPages.Clear();
_settingsSections.Clear();
_desktopComponents.Clear();
}
@@ -593,9 +608,16 @@ public sealed class PluginRuntimeService : IDisposable
{
_exportRegistry.ReplaceExports(loadedPlugin.Manifest.Id, loadedPlugin.ExportedServices);
foreach (var settingsPage in loadedPlugin.SettingsPages)
_settingsCatalogService.RegisterPluginSections(loadedPlugin.Manifest.Id, loadedPlugin.SettingsSections);
_settingsSections.RemoveAll(entry => string.Equals(
entry.Plugin.Manifest.Id,
loadedPlugin.Manifest.Id,
StringComparison.OrdinalIgnoreCase));
foreach (var settingsSection in loadedPlugin.SettingsSections)
{
_settingsPages.Add(new PluginSettingsPageContribution(loadedPlugin, settingsPage));
_settingsSections.Add(new PluginSettingsSectionContribution(loadedPlugin, settingsSection));
}
foreach (var desktopComponent in loadedPlugin.DesktopComponents)
@@ -769,9 +791,10 @@ public sealed class PluginRuntimeService : IDisposable
private void RemovePluginFromCatalog(string pluginId)
{
_catalog.RemoveAll(entry => string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_settingsPages.RemoveAll(entry => string.Equals(entry.Plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_settingsSections.RemoveAll(entry => string.Equals(entry.Plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_desktopComponents.RemoveAll(entry => string.Equals(entry.Plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_loadResults.RemoveAll(entry => string.Equals(entry.Manifest?.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_settingsCatalogService.RemovePluginSections(pluginId);
}
private sealed record PluginCandidate(
@@ -784,15 +807,21 @@ public sealed class PluginRuntimeService : IDisposable
private readonly IPluginPackageManager _packageManager;
private readonly IHostApplicationLifecycle _applicationLifecycle;
private readonly IPluginExportRegistry _exportRegistry;
private readonly ISettingsService _settingsService;
private readonly ISettingsCatalog _settingsCatalog;
public PluginHostServiceProvider(
IPluginPackageManager packageManager,
IHostApplicationLifecycle applicationLifecycle,
IPluginExportRegistry exportRegistry)
IPluginExportRegistry exportRegistry,
ISettingsService settingsService,
ISettingsCatalog settingsCatalog)
{
_packageManager = packageManager;
_applicationLifecycle = applicationLifecycle;
_exportRegistry = exportRegistry;
_settingsService = settingsService;
_settingsCatalog = settingsCatalog;
}
public object? GetService(Type serviceType)
@@ -812,6 +841,16 @@ public sealed class PluginRuntimeService : IDisposable
return _exportRegistry;
}
if (serviceType == typeof(ISettingsService))
{
return _settingsService;
}
if (serviceType == typeof(ISettingsCatalog))
{
return _settingsCatalog;
}
return null;
}
}

View File

@@ -75,11 +75,11 @@ public partial class PluginSettingsPage : UserControl
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}.",
"Detected {0} plugin(s); enabled {1}; loaded {2}; settings sections {3}; widgets {4}; failures {5}.",
runtime.Catalog.Count,
enabledCount,
runtime.Catalog.Count(entry => entry.IsLoaded),
runtime.SettingsPages.Count,
runtime.SettingsSections.Count,
runtime.DesktopComponents.Count,
failures.Length);
@@ -316,9 +316,6 @@ public partial class PluginSettingsPage : UserControl
case MainWindow mainWindow:
mainWindow.RefreshPluginSettingsNavigation();
break;
case SettingsWindow settingsWindow:
settingsWindow.RefreshPluginSettingsNavigation();
break;
}
}

View File

@@ -9,7 +9,7 @@ using System.Security.Cryptography;
using System.Threading;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views.SettingsPages;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Plugins;

View File

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

View File

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

View File

@@ -1,198 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using FluentIcons.Common;
using FluentAvalonia.UI.Controls;
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()
{
_pluginSettingsPageHosts.Clear();
_pluginSettingsNavItems.Clear();
}
private void RegisterPluginSettingsDefinitions()
{
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);
for (var i = 0; i < contributions.Length; i++)
{
var contribution = contributions[i];
var tag = BuildPluginSettingsTag(contribution);
_pluginSettingsPageHosts[tag] = CreatePluginSettingsPageHost(contribution);
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
tag,
BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId),
BuildPluginSettingsPageDescription(contribution),
FluentIcons.Common.Symbol.PuzzlePiece,
IndependentSettingsPageCategory.External,
200 + i,
$"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"));
}
}
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 string BuildPluginSettingsPageDescription(PluginSettingsPageContribution contribution)
{
return Lf(
"settings.page_desc.plugin_contributed_format",
"Settings page '{0}' is provided by plugin '{1}'.",
contribution.Registration.Title,
contribution.Plugin.Manifest.Name);
}
private Control CreatePluginSettingsPageHost(PluginSettingsPageContribution contribution)
{
Control content;
try
{
content = contribution.Registration.ContentFactory(contribution.Plugin.Services);
}
catch (Exception ex)
{
content = CreatePluginPageErrorContent(ex);
}
return new StackPanel
{
Spacing = 16,
MaxWidth = 920,
Children =
{
new TextBlock
{
Text = contribution.Registration.Title,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
Foreground = GetThemeBrush("TextFillColorPrimaryBrush")
},
new TextBlock
{
Text = contribution.Plugin.Manifest.Name,
Foreground = GetThemeBrush("TextFillColorSecondaryBrush")
},
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
}
};
}
internal void RefreshPluginSettingsNavigation()
{
var preferredTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
InitializeSettingsNavigation();
SelectSettingsTab(
_settingsPageDefinitions.ContainsKey(preferredTag) ? preferredTag : "Plugins",
persistSelection: false);
PluginSettingsPanel?.RefreshFromRuntime();
}
private string? GetSelectedSettingsTabTag()
{
return NormalizeSettingsPageTag(_selectedSettingsTabTag);
}
private int ResolveSelectedSettingsTabIndex()
{
if (SettingsNavView?.MenuItems is null)
{
return 0;
}
var items = SettingsNavView.MenuItems.OfType<NavigationViewItem>().ToList();
for (var i = 0; i < items.Count; i++)
{
if (string.Equals(items[i].Tag?.ToString(), NormalizeSettingsPageTag(_selectedSettingsTabTag), StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return 0;
}
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
var items = SettingsNavView.MenuItems.OfType<NavigationViewItem>().ToList();
if (items.Count == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag))
{
var normalizedTag = NormalizeSettingsPageTag(snapshot.SettingsTabTag);
var taggedItem = items
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), normalizedTag, StringComparison.OrdinalIgnoreCase));
if (taggedItem is not null)
{
_selectedSettingsTabTag = normalizedTag;
SettingsNavView.SelectedItem = taggedItem;
return;
}
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, items.Count - 1));
_selectedSettingsTabTag = items[safeIndex].Tag?.ToString() ?? _selectedSettingsTabTag;
SettingsNavView.SelectedItem = items[safeIndex];
}
}

View File

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