mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
setting_re2
设置架构革新中
This commit is contained in:
@@ -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,
|
||||
|
||||
24
LanMountainDesktop.PluginSdk/IComponentSettingsAccessor.cs
Normal file
24
LanMountainDesktop.PluginSdk/IComponentSettingsAccessor.cs
Normal 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);
|
||||
}
|
||||
10
LanMountainDesktop.PluginSdk/ISettingsCatalog.cs
Normal file
10
LanMountainDesktop.PluginSdk/ISettingsCatalog.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public interface ISettingsCatalog
|
||||
{
|
||||
IReadOnlyList<SettingsSectionDefinition> GetSections();
|
||||
|
||||
IReadOnlyList<SettingsSectionDefinition> GetSections(SettingsScope scope);
|
||||
}
|
||||
56
LanMountainDesktop.PluginSdk/ISettingsService.cs
Normal file
56
LanMountainDesktop.PluginSdk/ISettingsService.cs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
147
LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs
Normal file
147
LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
14
LanMountainDesktop.PluginSdk/SettingsCategories.cs
Normal file
14
LanMountainDesktop.PluginSdk/SettingsCategories.cs
Normal 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";
|
||||
}
|
||||
32
LanMountainDesktop.PluginSdk/SettingsChangedEvent.cs
Normal file
32
LanMountainDesktop.PluginSdk/SettingsChangedEvent.cs
Normal 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; }
|
||||
}
|
||||
17
LanMountainDesktop.PluginSdk/SettingsOptionChoice.cs
Normal file
17
LanMountainDesktop.PluginSdk/SettingsOptionChoice.cs
Normal 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; }
|
||||
}
|
||||
53
LanMountainDesktop.PluginSdk/SettingsOptionDefinition.cs
Normal file
53
LanMountainDesktop.PluginSdk/SettingsOptionDefinition.cs
Normal 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; }
|
||||
}
|
||||
11
LanMountainDesktop.PluginSdk/SettingsOptionType.cs
Normal file
11
LanMountainDesktop.PluginSdk/SettingsOptionType.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public enum SettingsOptionType
|
||||
{
|
||||
Toggle = 0,
|
||||
Select = 1,
|
||||
Text = 2,
|
||||
Number = 3,
|
||||
Path = 4,
|
||||
List = 5
|
||||
}
|
||||
9
LanMountainDesktop.PluginSdk/SettingsScope.cs
Normal file
9
LanMountainDesktop.PluginSdk/SettingsScope.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public enum SettingsScope
|
||||
{
|
||||
App = 0,
|
||||
Launcher = 1,
|
||||
Plugin = 2,
|
||||
ComponentInstance = 3
|
||||
}
|
||||
53
LanMountainDesktop.PluginSdk/SettingsSectionDefinition.cs
Normal file
53
LanMountainDesktop.PluginSdk/SettingsSectionDefinition.cs
Normal 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; }
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
namespace LanMountainDesktop.Models;
|
||||
namespace LanMountainDesktop.Models;
|
||||
|
||||
public enum TaskbarActionId
|
||||
{
|
||||
MinimizeToWindows,
|
||||
OpenSettings,
|
||||
AddDesktopPage,
|
||||
DeleteDesktopPage,
|
||||
DeleteComponent,
|
||||
EditComponent,
|
||||
HideLauncherEntry
|
||||
}
|
||||
|
||||
@@ -2,11 +2,5 @@
|
||||
|
||||
public enum TaskbarContext
|
||||
{
|
||||
Desktop,
|
||||
SettingsWallpaper,
|
||||
SettingsGrid,
|
||||
SettingsColor,
|
||||
SettingsStatusBar,
|
||||
SettingsWeather,
|
||||
SettingsRegion
|
||||
Desktop
|
||||
}
|
||||
|
||||
116
LanMountainDesktop/Services/DesktopGridLayoutService.cs
Normal file
116
LanMountainDesktop/Services/DesktopGridLayoutService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
189
LanMountainDesktop/Services/Settings/SettingsContracts.cs
Normal file
189
LanMountainDesktop/Services/Settings/SettingsContracts.cs
Normal 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; }
|
||||
}
|
||||
627
LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
Normal file
627
LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
423
LanMountainDesktop/Services/Settings/SettingsService.cs
Normal file
423
LanMountainDesktop/Services/Settings/SettingsService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class DateWidgetSettingsWindow : UserControl
|
||||
{
|
||||
public DateWidgetSettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
internal enum IndependentSettingsPageCategory
|
||||
{
|
||||
Internal = 0,
|
||||
External = 1,
|
||||
About = 2,
|
||||
Debug = 3
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
@@ -250,30 +245,14 @@ 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
|
||||
};
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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="设置">
|
||||
<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="返回桌面" />
|
||||
</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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
namespace LanMountainDesktop.Services.PluginMarket;
|
||||
|
||||
internal sealed class AirAppMarketCacheService
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.PluginMarket;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class SettingsWindow
|
||||
{
|
||||
private void ApplyPluginMarketSettingsLocalization()
|
||||
{
|
||||
PluginMarketSettingsPanel.RefreshFromRuntime();
|
||||
}
|
||||
}
|
||||
@@ -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")!;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user