mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915739ff7b | ||
|
|
cb86ca10e7 | ||
|
|
b3a74aa072 | ||
|
|
b436bfa884 | ||
|
|
081abeb688 | ||
|
|
594a62132f | ||
|
|
15e589aedd | ||
|
|
ac4617f5cf | ||
|
|
0645598753 |
16
.codex/environments/environment.toml
Normal file
16
.codex/environments/environment.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||||
|
version = 1
|
||||||
|
name = "LanMountainDesktop"
|
||||||
|
|
||||||
|
[setup]
|
||||||
|
script = ""
|
||||||
|
|
||||||
|
[[actions]]
|
||||||
|
name = "运行"
|
||||||
|
icon = "run"
|
||||||
|
command = "dotnet run --project 'C:\\Users\\USER693091\\Documents\\GitHub\\LanMountainDesktop\\LanMountainDesktop\\LanMountainDesktop.csproj"
|
||||||
|
|
||||||
|
[[actions]]
|
||||||
|
name = "构建"
|
||||||
|
icon = "tool"
|
||||||
|
command = "dotnet build 'C:\\Users\\USER693091\\Documents\\GitHub\\LanMountainDesktop\\LanMountainDesktop.slnx"
|
||||||
8
Directory.Build.props
Normal file
8
Directory.Build.props
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
|
||||||
|
<Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
|
||||||
|
<ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using LanMountainDesktop.Settings.Core;
|
||||||
|
using LanMountainDesktop.Shared.Contracts;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Appearance;
|
||||||
|
|
||||||
|
public static class AppearanceCornerRadiusTokenFactory
|
||||||
|
{
|
||||||
|
public static AppearanceCornerRadiusTokens Create(double scale)
|
||||||
|
{
|
||||||
|
var normalizedScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(scale);
|
||||||
|
return new AppearanceCornerRadiusTokens(
|
||||||
|
Radius(6, normalizedScale),
|
||||||
|
Radius(10, normalizedScale),
|
||||||
|
Radius(14, normalizedScale),
|
||||||
|
Radius(18, normalizedScale),
|
||||||
|
Radius(24, normalizedScale),
|
||||||
|
Radius(30, normalizedScale),
|
||||||
|
Radius(36, normalizedScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CornerRadius Radius(double value, double scale)
|
||||||
|
{
|
||||||
|
var scaled = Math.Round(value * scale * 2, MidpointRounding.AwayFromZero) / 2d;
|
||||||
|
return new CornerRadius(scaled);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
27
LanMountainDesktop.DesktopHost/DesktopBootstrap.cs
Normal file
27
LanMountainDesktop.DesktopHost/DesktopBootstrap.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.DesktopHost;
|
||||||
|
|
||||||
|
public static class DesktopBootstrap
|
||||||
|
{
|
||||||
|
public static void InitializeStartupServices(Action initializeDeviceId, Action initializeCrashReporting, Action initializeUserBehaviorAnalytics, Action scheduleStartupCleanup)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(initializeDeviceId);
|
||||||
|
ArgumentNullException.ThrowIfNull(initializeCrashReporting);
|
||||||
|
ArgumentNullException.ThrowIfNull(initializeUserBehaviorAnalytics);
|
||||||
|
ArgumentNullException.ThrowIfNull(scheduleStartupCleanup);
|
||||||
|
|
||||||
|
initializeDeviceId();
|
||||||
|
initializeCrashReporting();
|
||||||
|
initializeUserBehaviorAnalytics();
|
||||||
|
scheduleStartupCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void InitializeApplication(Application application, Action initializeShell)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(application);
|
||||||
|
ArgumentNullException.ThrowIfNull(initializeShell);
|
||||||
|
initializeShell();
|
||||||
|
}
|
||||||
|
}
|
||||||
55
LanMountainDesktop.DesktopHost/DesktopShellHost.cs
Normal file
55
LanMountainDesktop.DesktopHost/DesktopShellHost.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.DesktopHost;
|
||||||
|
|
||||||
|
public sealed class DesktopShellHost : IDesktopShellHost
|
||||||
|
{
|
||||||
|
private readonly Action _initializePluginRuntime;
|
||||||
|
private readonly Action _initializeTrayIcon;
|
||||||
|
private readonly Action<IClassicDesktopStyleApplicationLifetime> _createAndAssignMainWindow;
|
||||||
|
private readonly Action _performExitCleanup;
|
||||||
|
private readonly Action _startActivationListener;
|
||||||
|
private readonly Action _startWeatherRefresh;
|
||||||
|
|
||||||
|
public DesktopShellHost(
|
||||||
|
Action initializePluginRuntime,
|
||||||
|
Action initializeTrayIcon,
|
||||||
|
Action<IClassicDesktopStyleApplicationLifetime> createAndAssignMainWindow,
|
||||||
|
Action performExitCleanup,
|
||||||
|
Action startActivationListener,
|
||||||
|
Action startWeatherRefresh)
|
||||||
|
{
|
||||||
|
_initializePluginRuntime = initializePluginRuntime;
|
||||||
|
_initializeTrayIcon = initializeTrayIcon;
|
||||||
|
_createAndAssignMainWindow = createAndAssignMainWindow;
|
||||||
|
_performExitCleanup = performExitCleanup;
|
||||||
|
_startActivationListener = startActivationListener;
|
||||||
|
_startWeatherRefresh = startWeatherRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("An application instance is required to initialize the desktop shell.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(Application application)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(application);
|
||||||
|
|
||||||
|
_initializePluginRuntime();
|
||||||
|
_initializeTrayIcon();
|
||||||
|
|
||||||
|
if (application.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.Exit += (_, _) => _performExitCleanup();
|
||||||
|
_createAndAssignMainWindow(desktop);
|
||||||
|
_startActivationListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
_startWeatherRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
LanMountainDesktop.DesktopHost/DesktopStartupCoordinator.cs
Normal file
15
LanMountainDesktop.DesktopHost/DesktopStartupCoordinator.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.DesktopHost;
|
||||||
|
|
||||||
|
public sealed class DesktopStartupCoordinator
|
||||||
|
{
|
||||||
|
private readonly Action _restoreWorkspaceState;
|
||||||
|
|
||||||
|
public DesktopStartupCoordinator(Action restoreWorkspaceState)
|
||||||
|
{
|
||||||
|
_restoreWorkspaceState = restoreWorkspaceState ?? throw new ArgumentNullException(nameof(restoreWorkspaceState));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Restore() => _restoreWorkspaceState();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.DesktopComponents.Runtime\LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
18
LanMountainDesktop.DesktopHost/SettingsWindowHost.cs
Normal file
18
LanMountainDesktop.DesktopHost/SettingsWindowHost.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.DesktopHost;
|
||||||
|
|
||||||
|
public sealed class SettingsWindowHost
|
||||||
|
{
|
||||||
|
private readonly Action<string, string?> _openSettingsWindow;
|
||||||
|
|
||||||
|
public SettingsWindowHost(Action<string, string?> openSettingsWindow)
|
||||||
|
{
|
||||||
|
_openSettingsWindow = openSettingsWindow ?? throw new ArgumentNullException(nameof(openSettingsWindow));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Open(string source, string? pageId = null)
|
||||||
|
{
|
||||||
|
_openSettingsWindow(source, pageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
LanMountainDesktop.DesktopHost/ShutdownCoordinator.cs
Normal file
19
LanMountainDesktop.DesktopHost/ShutdownCoordinator.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.DesktopHost;
|
||||||
|
|
||||||
|
public sealed class ShutdownCoordinator
|
||||||
|
{
|
||||||
|
private readonly Action<bool, string> _prepareForShutdown;
|
||||||
|
private readonly Action<string> _resetShutdownIntent;
|
||||||
|
|
||||||
|
public ShutdownCoordinator(Action<bool, string> prepareForShutdown, Action<string> resetShutdownIntent)
|
||||||
|
{
|
||||||
|
_prepareForShutdown = prepareForShutdown ?? throw new ArgumentNullException(nameof(prepareForShutdown));
|
||||||
|
_resetShutdownIntent = resetShutdownIntent ?? throw new ArgumentNullException(nameof(resetShutdownIntent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Prepare(bool isRestart, string source) => _prepareForShutdown(isRestart, source);
|
||||||
|
|
||||||
|
public void Reset(string source) => _resetShutdownIntent(source);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
using LanMountainDesktop.Shared.Contracts;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Host.Abstractions;
|
||||||
|
|
||||||
|
public sealed record ComponentChromeContext(
|
||||||
|
string ComponentId,
|
||||||
|
string? PlacementId,
|
||||||
|
double CellSize,
|
||||||
|
double GlobalCornerRadiusScale,
|
||||||
|
AppearanceCornerRadiusTokens CornerRadiusTokens,
|
||||||
|
SettingsScope Scope = SettingsScope.App);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace LanMountainDesktop.Host.Abstractions;
|
||||||
|
|
||||||
|
public interface IDesktopShellHost
|
||||||
|
{
|
||||||
|
void Initialize();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using LanMountainDesktop.Shared.Contracts;
|
||||||
|
|
||||||
namespace LanMountainDesktop.PluginSdk;
|
namespace LanMountainDesktop.PluginSdk;
|
||||||
|
|
||||||
public sealed class PluginDesktopComponentContext
|
public sealed class PluginDesktopComponentContext
|
||||||
@@ -11,6 +13,8 @@ public sealed class PluginDesktopComponentContext
|
|||||||
string componentId,
|
string componentId,
|
||||||
string? placementId,
|
string? placementId,
|
||||||
double cellSize,
|
double cellSize,
|
||||||
|
double globalCornerRadiusScale,
|
||||||
|
AppearanceCornerRadiusTokens cornerRadiusTokens,
|
||||||
IPluginSettingsService? pluginSettings = null)
|
IPluginSettingsService? pluginSettings = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(manifest);
|
ArgumentNullException.ThrowIfNull(manifest);
|
||||||
@@ -19,6 +23,7 @@ public sealed class PluginDesktopComponentContext
|
|||||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
|
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
ArgumentNullException.ThrowIfNull(properties);
|
ArgumentNullException.ThrowIfNull(properties);
|
||||||
|
ArgumentNullException.ThrowIfNull(cornerRadiusTokens);
|
||||||
|
|
||||||
Manifest = manifest;
|
Manifest = manifest;
|
||||||
PluginDirectory = pluginDirectory;
|
PluginDirectory = pluginDirectory;
|
||||||
@@ -28,6 +33,8 @@ public sealed class PluginDesktopComponentContext
|
|||||||
ComponentId = componentId.Trim();
|
ComponentId = componentId.Trim();
|
||||||
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
|
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
|
||||||
CellSize = Math.Max(1, cellSize);
|
CellSize = Math.Max(1, cellSize);
|
||||||
|
GlobalCornerRadiusScale = Math.Max(0.1d, globalCornerRadiusScale);
|
||||||
|
CornerRadiusTokens = cornerRadiusTokens;
|
||||||
PluginSettings = pluginSettings;
|
PluginSettings = pluginSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +54,22 @@ public sealed class PluginDesktopComponentContext
|
|||||||
|
|
||||||
public double CellSize { get; }
|
public double CellSize { get; }
|
||||||
|
|
||||||
|
public double GlobalCornerRadiusScale { get; }
|
||||||
|
|
||||||
|
public AppearanceCornerRadiusTokens CornerRadiusTokens { get; }
|
||||||
|
|
||||||
public IPluginSettingsService? PluginSettings { get; }
|
public IPluginSettingsService? PluginSettings { get; }
|
||||||
|
|
||||||
|
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
|
||||||
|
{
|
||||||
|
var scaled = Math.Max(0d, baseRadius) * GlobalCornerRadiusScale;
|
||||||
|
var scaledMin = minimum.HasValue ? minimum.Value * GlobalCornerRadiusScale : scaled;
|
||||||
|
var scaledMax = maximum.HasValue ? maximum.Value * GlobalCornerRadiusScale : scaled;
|
||||||
|
return minimum.HasValue || maximum.HasValue
|
||||||
|
? Math.Clamp(scaled, scaledMin, scaledMax)
|
||||||
|
: scaled;
|
||||||
|
}
|
||||||
|
|
||||||
public T? GetService<T>()
|
public T? GetService<T>()
|
||||||
{
|
{
|
||||||
return (T?)Services.GetService(typeof(T));
|
return (T?)Services.GetService(typeof(T));
|
||||||
|
|||||||
20
LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs
Normal file
20
LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace LanMountainDesktop.Settings.Core;
|
||||||
|
|
||||||
|
public static class GlobalAppearanceSettings
|
||||||
|
{
|
||||||
|
public const double DefaultCornerRadiusScale = 1.0;
|
||||||
|
public const double MinimumCornerRadiusScale = 0.70;
|
||||||
|
public const double MaximumCornerRadiusScale = 1.40;
|
||||||
|
public const double CornerRadiusScaleStep = 0.05;
|
||||||
|
|
||||||
|
public static double NormalizeCornerRadiusScale(double value)
|
||||||
|
{
|
||||||
|
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||||
|
{
|
||||||
|
return DefaultCornerRadiusScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clamped = Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale);
|
||||||
|
return Math.Round(clamped / CornerRadiusScaleStep, MidpointRounding.AwayFromZero) * CornerRadiusScaleStep;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Avalonia;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Shared.Contracts;
|
||||||
|
|
||||||
|
public sealed record AppearanceCornerRadiusTokens(
|
||||||
|
CornerRadius Micro,
|
||||||
|
CornerRadius Xs,
|
||||||
|
CornerRadius Sm,
|
||||||
|
CornerRadius Md,
|
||||||
|
CornerRadius Lg,
|
||||||
|
CornerRadius Xl,
|
||||||
|
CornerRadius Island);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Tests;
|
||||||
|
|
||||||
|
public sealed class WhiteboardNotePersistenceServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SaveNote_ThenLoadNote_RoundTripsSnapshot()
|
||||||
|
{
|
||||||
|
using var sandbox = new WhiteboardNotePersistenceSandbox();
|
||||||
|
var service = sandbox.CreateService();
|
||||||
|
var snapshot = CreateSampleSnapshot();
|
||||||
|
|
||||||
|
service.SaveNote("DesktopWhiteboard", "whiteboard-1", snapshot, retentionDays: 15);
|
||||||
|
|
||||||
|
var loaded = service.LoadNote("DesktopWhiteboard", "whiteboard-1", retentionDays: 15);
|
||||||
|
|
||||||
|
Assert.Single(loaded.Strokes);
|
||||||
|
Assert.Equal(2, loaded.Strokes[0].Points.Count);
|
||||||
|
Assert.Equal("#FF112233", loaded.Strokes[0].Color);
|
||||||
|
Assert.True(loaded.SavedUtc > DateTimeOffset.MinValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadNote_RemovesExpiredSnapshot_WhenRetentionExceeded()
|
||||||
|
{
|
||||||
|
using var sandbox = new WhiteboardNotePersistenceSandbox();
|
||||||
|
var service = sandbox.CreateService();
|
||||||
|
|
||||||
|
service.SaveNote("DesktopWhiteboard", "expired-board", CreateSampleSnapshot(), retentionDays: 7);
|
||||||
|
sandbox.OverrideSavedTimestamp("DesktopWhiteboard", "expired-board", DateTimeOffset.UtcNow.AddDays(-10), retentionDays: 7);
|
||||||
|
|
||||||
|
var loaded = service.LoadNote("DesktopWhiteboard", "expired-board", retentionDays: 7);
|
||||||
|
|
||||||
|
Assert.Empty(loaded.Strokes);
|
||||||
|
Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-board"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteExpiredNotesBatch_RemovesExpiredRows_AndKeepsFreshRows()
|
||||||
|
{
|
||||||
|
using var sandbox = new WhiteboardNotePersistenceSandbox();
|
||||||
|
var service = sandbox.CreateService();
|
||||||
|
|
||||||
|
service.SaveNote("DesktopWhiteboard", "expired-a", CreateSampleSnapshot(), retentionDays: 7);
|
||||||
|
service.SaveNote("DesktopWhiteboard", "expired-b", CreateSampleSnapshot(), retentionDays: 7);
|
||||||
|
service.SaveNote("DesktopWhiteboard", "fresh-c", CreateSampleSnapshot(), retentionDays: 15);
|
||||||
|
|
||||||
|
sandbox.OverrideSavedTimestamp("DesktopWhiteboard", "expired-a", DateTimeOffset.UtcNow.AddDays(-9), retentionDays: 7);
|
||||||
|
sandbox.OverrideSavedTimestamp("DesktopWhiteboard", "expired-b", DateTimeOffset.UtcNow.AddDays(-8), retentionDays: 7);
|
||||||
|
sandbox.OverrideSavedTimestamp("DesktopWhiteboard", "fresh-c", DateTimeOffset.UtcNow.AddDays(-2), retentionDays: 15);
|
||||||
|
|
||||||
|
var deletedCount = service.DeleteExpiredNotesBatch(batchSize: 10);
|
||||||
|
|
||||||
|
Assert.Equal(2, deletedCount);
|
||||||
|
Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-a"));
|
||||||
|
Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-b"));
|
||||||
|
Assert.True(sandbox.Exists("DesktopWhiteboard", "fresh-c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WhiteboardNoteSnapshot CreateSampleSnapshot()
|
||||||
|
{
|
||||||
|
return new WhiteboardNoteSnapshot
|
||||||
|
{
|
||||||
|
Strokes =
|
||||||
|
[
|
||||||
|
new WhiteboardStrokeSnapshot
|
||||||
|
{
|
||||||
|
Color = "#FF112233",
|
||||||
|
InkThickness = 3.5d,
|
||||||
|
IgnorePressure = true,
|
||||||
|
Points =
|
||||||
|
[
|
||||||
|
new WhiteboardStylusPointSnapshot { X = 12, Y = 34, Pressure = 0.4d, Width = 2, Height = 2 },
|
||||||
|
new WhiteboardStylusPointSnapshot { X = 48, Y = 64, Pressure = 0.7d, Width = 2, Height = 2 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class WhiteboardNotePersistenceSandbox : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _directoryPath = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
"LanMountainDesktop.WhiteboardNoteTests",
|
||||||
|
Guid.NewGuid().ToString("N"));
|
||||||
|
|
||||||
|
private readonly string _databasePath;
|
||||||
|
|
||||||
|
public WhiteboardNotePersistenceSandbox()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_directoryPath);
|
||||||
|
_databasePath = Path.Combine(_directoryPath, "whiteboard-tests.db");
|
||||||
|
}
|
||||||
|
|
||||||
|
public WhiteboardNotePersistenceService CreateService()
|
||||||
|
{
|
||||||
|
return new WhiteboardNotePersistenceService(new AppDatabaseService(_databasePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OverrideSavedTimestamp(string componentId, string placementId, DateTimeOffset savedUtc, int retentionDays)
|
||||||
|
{
|
||||||
|
var expiresUtc = savedUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
|
||||||
|
using var connection = new AppDatabaseService(_databasePath).OpenConnection();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = """
|
||||||
|
UPDATE whiteboard_notes
|
||||||
|
SET saved_at_utc_ms = $savedAtUtcMs,
|
||||||
|
expires_at_utc_ms = $expiresAtUtcMs,
|
||||||
|
updated_at_utc_ms = $updatedAtUtcMs
|
||||||
|
WHERE component_id = $componentId
|
||||||
|
AND placement_id = $placementId;
|
||||||
|
""";
|
||||||
|
command.Parameters.AddWithValue("$savedAtUtcMs", savedUtc.ToUnixTimeMilliseconds());
|
||||||
|
command.Parameters.AddWithValue("$expiresAtUtcMs", expiresUtc.ToUnixTimeMilliseconds());
|
||||||
|
command.Parameters.AddWithValue("$updatedAtUtcMs", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
|
||||||
|
command.Parameters.AddWithValue("$componentId", componentId);
|
||||||
|
command.Parameters.AddWithValue("$placementId", placementId);
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists(string componentId, string placementId)
|
||||||
|
{
|
||||||
|
using var connection = new AppDatabaseService(_databasePath).OpenConnection();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = """
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM whiteboard_notes
|
||||||
|
WHERE component_id = $componentId
|
||||||
|
AND placement_id = $placementId;
|
||||||
|
""";
|
||||||
|
command.Parameters.AddWithValue("$componentId", componentId);
|
||||||
|
command.Parameters.AddWithValue("$placementId", placementId);
|
||||||
|
return Convert.ToInt32(command.ExecuteScalar()) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_directoryPath))
|
||||||
|
{
|
||||||
|
Directory.Delete(_directoryPath, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Temporary test directories are best-effort cleanup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="LanAirApp/tools/LanMountainDesktop.PluginPackager/LanMountainDesktop.PluginPackager.csproj" />
|
<Project Path="LanAirApp/tools/LanMountainDesktop.PluginPackager/LanMountainDesktop.PluginPackager.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.Settings.Core/LanMountainDesktop.Settings.Core.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.Appearance/LanMountainDesktop.Appearance.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.DesktopComponents.Runtime/LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.DesktopHost/LanMountainDesktop.DesktopHost.csproj" />
|
||||||
<Project Path="LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj" />
|
<Project Path="LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj" />
|
||||||
<Project Path="LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj" />
|
<Project Path="LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj" />
|
||||||
<Project Path="LanMountainDesktop/LanMountainDesktop.csproj" />
|
<Project Path="LanMountainDesktop/LanMountainDesktop.csproj" />
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using Avalonia.Styling;
|
|||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using AvaloniaWebView;
|
using AvaloniaWebView;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.DesktopHost;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -61,6 +62,7 @@ public partial class App : Application
|
|||||||
private MainWindow? _mainWindow;
|
private MainWindow? _mainWindow;
|
||||||
private bool _mainWindowClosed;
|
private bool _mainWindowClosed;
|
||||||
private bool _uiUnhandledExceptionHooked;
|
private bool _uiUnhandledExceptionHooked;
|
||||||
|
private DesktopShellHost? _desktopShellHost;
|
||||||
|
|
||||||
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
|
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
|
||||||
internal static (UserBehaviorAnalyticsService?, CrashReportService?) AnalyticsServices { get; set; }
|
internal static (UserBehaviorAnalyticsService?, CrashReportService?) AnalyticsServices { get; set; }
|
||||||
@@ -116,28 +118,32 @@ public partial class App : Application
|
|||||||
AppLogger.Info("App", "Framework initialization completed.");
|
AppLogger.Info("App", "Framework initialization completed.");
|
||||||
RegisterUiUnhandledExceptionGuard();
|
RegisterUiUnhandledExceptionGuard();
|
||||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||||
InitializePluginRuntime();
|
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
||||||
InitializeTrayIcon();
|
|
||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeDesktopShell()
|
||||||
|
{
|
||||||
|
_desktopShellHost ??= new DesktopShellHost(
|
||||||
|
InitializePluginRuntime,
|
||||||
|
InitializeTrayIcon,
|
||||||
|
desktop =>
|
||||||
{
|
{
|
||||||
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
|
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
|
||||||
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
|
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
|
||||||
DisableAvaloniaDataAnnotationValidation();
|
DisableAvaloniaDataAnnotationValidation();
|
||||||
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
|
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
|
||||||
desktop.Exit += (_, _) =>
|
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
|
||||||
|
},
|
||||||
|
() =>
|
||||||
{
|
{
|
||||||
AppLogger.Info("App", "Desktop lifetime exit triggered.");
|
AppLogger.Info("App", "Desktop lifetime exit triggered.");
|
||||||
PerformExitCleanup();
|
PerformExitCleanup();
|
||||||
};
|
},
|
||||||
|
() => CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow),
|
||||||
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
|
StartWeatherLocationRefreshIfNeeded);
|
||||||
CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow);
|
_desktopShellHost.Initialize(this);
|
||||||
}
|
|
||||||
|
|
||||||
StartWeatherLocationRefreshIfNeeded();
|
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTrayExitClick(object? sender, EventArgs e)
|
private void OnTrayExitClick(object? sender, EventArgs e)
|
||||||
@@ -493,6 +499,7 @@ public partial class App : Application
|
|||||||
refreshAll ||
|
refreshAll ||
|
||||||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
|
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
|
||||||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) ||
|
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) ||
|
||||||
|
changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) ||
|
||||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
|
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
|
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
|
||||||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
|
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
|
||||||
@@ -510,6 +517,8 @@ public partial class App : Application
|
|||||||
|
|
||||||
if (languageChanged)
|
if (languageChanged)
|
||||||
{
|
{
|
||||||
|
// 清除本地化缓存,强制重新加载语言文件
|
||||||
|
_localizationService.ClearCache();
|
||||||
ApplyCurrentCultureFromSettings();
|
ApplyCurrentCultureFromSettings();
|
||||||
if (_trayIcons is not null)
|
if (_trayIcons is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# LanMountainDesktop 隐私政策
|
# LanMountainDesktop 隐私政策
|
||||||
|
|
||||||
**最后更新日期:2024年**
|
**最后更新日期:2026年3月17日**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -321,6 +321,6 @@ a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**感谢您信任 LanMountainDesktop!**
|
**感谢您信任阑山桌面LanMountainDesktop!**
|
||||||
|
|
||||||
我们承诺保护您的隐私,并持续改进我们的隐私保护措施。
|
我们承诺保护您的隐私,并持续改进我们的隐私保护措施。
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ public static class BuiltInComponentIds
|
|||||||
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
|
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
|
||||||
public const string DesktopBrowser = "DesktopBrowser";
|
public const string DesktopBrowser = "DesktopBrowser";
|
||||||
public const string DesktopOfficeRecentDocuments = "DesktopOfficeRecentDocuments";
|
public const string DesktopOfficeRecentDocuments = "DesktopOfficeRecentDocuments";
|
||||||
|
public const string DesktopRemovableStorage = "DesktopRemovableStorage";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,15 @@ public sealed class ComponentRegistry
|
|||||||
MinHeightCells: 2,
|
MinHeightCells: 2,
|
||||||
AllowStatusBarPlacement: false,
|
AllowStatusBarPlacement: false,
|
||||||
AllowDesktopPlacement: true),
|
AllowDesktopPlacement: true),
|
||||||
|
new DesktopComponentDefinition(
|
||||||
|
BuiltInComponentIds.DesktopRemovableStorage,
|
||||||
|
"Removable Storage",
|
||||||
|
"Storage",
|
||||||
|
"File",
|
||||||
|
MinWidthCells: 2,
|
||||||
|
MinHeightCells: 2,
|
||||||
|
AllowStatusBarPlacement: false,
|
||||||
|
AllowDesktopPlacement: true),
|
||||||
new DesktopComponentDefinition(
|
new DesktopComponentDefinition(
|
||||||
BuiltInComponentIds.Date,
|
BuiltInComponentIds.Date,
|
||||||
"Calendar",
|
"Calendar",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
@@ -10,5 +11,6 @@ public sealed record DesktopComponentRuntimeContext(
|
|||||||
ISettingsFacadeService SettingsFacade,
|
ISettingsFacadeService SettingsFacade,
|
||||||
ISettingsService SettingsService,
|
ISettingsService SettingsService,
|
||||||
IAppearanceThemeService AppearanceTheme,
|
IAppearanceThemeService AppearanceTheme,
|
||||||
|
ComponentChromeContext Chrome,
|
||||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
IComponentInstanceSettingsStore ComponentSettingsStore);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.ComponentSystem;
|
||||||
|
|
||||||
|
public interface IComponentChromeContextAware
|
||||||
|
{
|
||||||
|
void SetComponentChromeContext(ComponentChromeContext context);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
@@ -21,12 +21,20 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Models\" />
|
<Folder Include="Models\" />
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
|
<AvaloniaResource Include="Localization\**" />
|
||||||
<EmbeddedResource Include="Assets\Documents\Privacy.md" />
|
<EmbeddedResource Include="Assets\Documents\Privacy.md" />
|
||||||
|
<EmbeddedResource Include="Localization\*.json" />
|
||||||
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.DesktopComponents.Runtime\LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.DesktopHost\LanMountainDesktop.DesktopHost.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||||
<ProjectReference Include="..\LanMountainDesktop.PluginsInstallHelper\LanMountainDesktop.PluginsInstallHelper.csproj" ReferenceOutputAssembly="false" />
|
<ProjectReference Include="..\LanMountainDesktop.PluginsInstallHelper\LanMountainDesktop.PluginsInstallHelper.csproj" ReferenceOutputAssembly="false" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -53,6 +61,10 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||||
|
<PackageReference Include="MudTools.OfficeInterop" Version="2.0.8" />
|
||||||
|
<PackageReference Include="MudTools.OfficeInterop.Word" Version="2.0.8" />
|
||||||
|
<PackageReference Include="MudTools.OfficeInterop.Excel" Version="2.0.8" />
|
||||||
|
<PackageReference Include="MudTools.OfficeInterop.PowerPoint" Version="2.0.8" />
|
||||||
|
|
||||||
<PackageReference Include="PortAudioSharp2" Version="1.0.6" />
|
<PackageReference Include="PortAudioSharp2" Version="1.0.6" />
|
||||||
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
|
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
"tray.menu.restart": "Restart App",
|
"tray.menu.restart": "Restart App",
|
||||||
"tray.menu.exit": "Exit App",
|
"tray.menu.exit": "Exit App",
|
||||||
"button.back_to_windows": "Back to Windows",
|
"button.back_to_windows": "Back to Windows",
|
||||||
|
"button.back_to_platform": "Back to {0}",
|
||||||
"tooltip.back_to_windows": "Back to Windows",
|
"tooltip.back_to_windows": "Back to Windows",
|
||||||
|
"tooltip.back_to_platform": "Back to {0}",
|
||||||
|
"platform.windows": "Windows",
|
||||||
|
"platform.linux": "Linux",
|
||||||
|
"platform.macos": "macOS",
|
||||||
"tooltip.open_settings": "Settings",
|
"tooltip.open_settings": "Settings",
|
||||||
"settings.title": "Settings",
|
"settings.title": "Settings",
|
||||||
"settings.shell.title": "Settings",
|
"settings.shell.title": "Settings",
|
||||||
@@ -20,7 +25,7 @@
|
|||||||
"settings.nav.group_system": "System",
|
"settings.nav.group_system": "System",
|
||||||
"settings.nav.group_extensions": "Extensions",
|
"settings.nav.group_extensions": "Extensions",
|
||||||
"settings.nav.wallpaper": "Wallpaper",
|
"settings.nav.wallpaper": "Wallpaper",
|
||||||
"settings.nav.grid": "Grid",
|
"settings.nav.grid": "Components",
|
||||||
"settings.nav.color": "Color",
|
"settings.nav.color": "Color",
|
||||||
"settings.nav.status_bar": "Status Bar",
|
"settings.nav.status_bar": "Status Bar",
|
||||||
"settings.nav.weather": "Weather",
|
"settings.nav.weather": "Weather",
|
||||||
@@ -86,6 +91,8 @@
|
|||||||
"settings.status_bar.description": "Choose which components appear on the top status bar.",
|
"settings.status_bar.description": "Choose which components appear on the top status bar.",
|
||||||
"settings.status_bar.clock_header": "Clock Component",
|
"settings.status_bar.clock_header": "Clock Component",
|
||||||
"settings.status_bar.clock_description": "Display a clock on the top status bar.",
|
"settings.status_bar.clock_description": "Display a clock on the top status bar.",
|
||||||
|
"settings.status_bar.clock_transparent_background_label": "Transparent background",
|
||||||
|
"settings.status_bar.clock_transparent_background_desc": "Remove the capsule background and keep only the clock text.",
|
||||||
"settings.status_bar.spacing_header": "Component Spacing",
|
"settings.status_bar.spacing_header": "Component Spacing",
|
||||||
"settings.status_bar.spacing_desc": "Adjust spacing between status bar components.",
|
"settings.status_bar.spacing_desc": "Adjust spacing between status bar components.",
|
||||||
"settings.status_bar.spacing_mode_compact": "Compact",
|
"settings.status_bar.spacing_mode_compact": "Compact",
|
||||||
@@ -296,8 +303,17 @@
|
|||||||
"settings.status_bar.clock_format.hm": "Hour:Minute",
|
"settings.status_bar.clock_format.hm": "Hour:Minute",
|
||||||
"settings.status_bar.clock_format.hms": "Hour:Minute:Second",
|
"settings.status_bar.clock_format.hms": "Hour:Minute:Second",
|
||||||
"settings.components.title": "Components",
|
"settings.components.title": "Components",
|
||||||
"settings.components.description": "Adjust desktop grid density and widget placement.",
|
"settings.components.description": "Adjust component layout and corner design.",
|
||||||
"settings.components.grid_header": "Grid Layout",
|
"settings.components.grid_header": "Grid Settings",
|
||||||
|
"settings.components.header": "Grid Settings",
|
||||||
|
"settings.components.short_side_label": "Short Side Cells",
|
||||||
|
"settings.components.edge_inset_label": "Screen Inset",
|
||||||
|
"settings.components.spacing_label": "Component Spacing",
|
||||||
|
"settings.components.spacing_compact": "Compact",
|
||||||
|
"settings.components.spacing_relaxed": "Relaxed",
|
||||||
|
"settings.components.corner_radius.header": "Corner Design",
|
||||||
|
"settings.components.corner_radius.label": "Component Corner Radius",
|
||||||
|
"settings.components.corner_radius.description": "Adjust the shared corner radius used by component containers, and expand the internal safe area with it.",
|
||||||
"settings.update.title": "Update",
|
"settings.update.title": "Update",
|
||||||
"settings.update.current_version_label": "Current Version",
|
"settings.update.current_version_label": "Current Version",
|
||||||
"settings.update.latest_version_label": "Latest Release",
|
"settings.update.latest_version_label": "Latest Release",
|
||||||
@@ -403,6 +419,7 @@
|
|||||||
"common.monet": "Monet",
|
"common.monet": "Monet",
|
||||||
"desktop.page_index_format": "Desktop {0}",
|
"desktop.page_index_format": "Desktop {0}",
|
||||||
"launcher.title": "App Launcher",
|
"launcher.title": "App Launcher",
|
||||||
|
"launcher.folder": "Folder",
|
||||||
"launcher.subtitle": "Apps and folders from Windows Start Menu",
|
"launcher.subtitle": "Apps and folders from Windows Start Menu",
|
||||||
"launcher.subtitle_linux": "Installed apps discovered from Linux desktop entries",
|
"launcher.subtitle_linux": "Installed apps discovered from Linux desktop entries",
|
||||||
"launcher.empty": "No Start Menu entries found.",
|
"launcher.empty": "No Start Menu entries found.",
|
||||||
@@ -559,6 +576,7 @@
|
|||||||
"component_category.info": "Info",
|
"component_category.info": "Info",
|
||||||
"component_category.calculator": "Calculator",
|
"component_category.calculator": "Calculator",
|
||||||
"component_category.study": "Study",
|
"component_category.study": "Study",
|
||||||
|
"component_category.file": "File",
|
||||||
"component.date": "Calendar",
|
"component.date": "Calendar",
|
||||||
"component.month_calendar": "Month Calendar",
|
"component.month_calendar": "Month Calendar",
|
||||||
"component.lunar_calendar": "Lunar Calendar",
|
"component.lunar_calendar": "Lunar Calendar",
|
||||||
@@ -587,6 +605,19 @@
|
|||||||
"component.blackboard_landscape": "Blackboard (Landscape)",
|
"component.blackboard_landscape": "Blackboard (Landscape)",
|
||||||
"component.browser": "Browser",
|
"component.browser": "Browser",
|
||||||
"component.office_recent_documents": "Recent Documents",
|
"component.office_recent_documents": "Recent Documents",
|
||||||
|
"whiteboard.settings.desc": "Each blackboard keeps its own note history and saves it independently.",
|
||||||
|
"whiteboard.settings.retention.title": "Note retention",
|
||||||
|
"whiteboard.settings.retention.desc": "Choose how long this blackboard should keep saved notes before expired data is removed automatically.",
|
||||||
|
"whiteboard.settings.retention.option": "{0} days",
|
||||||
|
"whiteboard.settings.instance_scope": "This retention setting is stored per blackboard component instance.",
|
||||||
|
"office_recent_documents.settings.desc": "Choose which Windows and Office sources this widget should scan for recent documents.",
|
||||||
|
"office_recent_documents.settings.sources_title": "Recent document sources",
|
||||||
|
"office_recent_documents.settings.sources_desc": "You can combine multiple sources. Registry selection also keeps the Office interop MRU fallback available.",
|
||||||
|
"office_recent_documents.settings.source.registry": "Office registry MRU",
|
||||||
|
"office_recent_documents.settings.source.recent_folders": "Windows Recent folders",
|
||||||
|
"office_recent_documents.settings.source.jump_lists": "Windows Jump Lists",
|
||||||
|
"office_recent_documents.settings.hint": "If you disable all sources, this widget will stay empty until at least one source is enabled again.",
|
||||||
|
"component.removable_storage": "Removable Storage",
|
||||||
"component.holiday_calendar": "Holiday Calendar",
|
"component.holiday_calendar": "Holiday Calendar",
|
||||||
"component.study_environment": "Environment",
|
"component.study_environment": "Environment",
|
||||||
"component.study_session_control": "Study Session Control",
|
"component.study_session_control": "Study Session Control",
|
||||||
@@ -788,6 +819,20 @@
|
|||||||
"study.environment.settings.show_display_db": "Show display dB",
|
"study.environment.settings.show_display_db": "Show display dB",
|
||||||
"study.environment.settings.show_dbfs": "Show dBFS",
|
"study.environment.settings.show_dbfs": "Show dBFS",
|
||||||
"study.environment.settings.hint": "At least one display mode must stay enabled.",
|
"study.environment.settings.hint": "At least one display mode must stay enabled.",
|
||||||
|
"removable_storage.settings.desc": "Show a connected USB drive with quick open and eject actions.",
|
||||||
|
"removable_storage.settings.behavior_title": "Behavior",
|
||||||
|
"removable_storage.settings.behavior_desc": "The widget automatically watches for removable drives and switches to the newest inserted USB drive.",
|
||||||
|
"removable_storage.action.open": "Open",
|
||||||
|
"removable_storage.action.eject": "Eject",
|
||||||
|
"removable_storage.widget.default_name": "Removable Drive",
|
||||||
|
"removable_storage.widget.empty_title": "No device inserted",
|
||||||
|
"removable_storage.widget.empty_subtitle": "Insert a USB drive to show it here.",
|
||||||
|
"removable_storage.widget.empty_hint": "Buttons stay disabled until a removable device is inserted.",
|
||||||
|
"removable_storage.widget.ready": "Ready to open or eject.",
|
||||||
|
"removable_storage.widget.ejecting": "Ejecting drive...",
|
||||||
|
"removable_storage.widget.eject_failed": "Could not eject this drive. Close any files on it and try again.",
|
||||||
|
"removable_storage.widget.open_failed": "Failed to open this drive.",
|
||||||
|
"removable_storage.widget.refresh_failed": "Drive list refresh failed.",
|
||||||
"study.session_control.action.start": "Start Study Session",
|
"study.session_control.action.start": "Start Study Session",
|
||||||
"study.session_control.action.stop": "Stop Study Session",
|
"study.session_control.action.stop": "Stop Study Session",
|
||||||
"study.session_control.idle_hint": "Tap the right button to start",
|
"study.session_control.idle_hint": "Tap the right button to start",
|
||||||
@@ -892,5 +937,7 @@
|
|||||||
"placement.tile": "Tile",
|
"placement.tile": "Tile",
|
||||||
"single_instance.notice.title": "App already running",
|
"single_instance.notice.title": "App already running",
|
||||||
"single_instance.notice.description": "The app is already running. There is no need to click multiple times to open it.",
|
"single_instance.notice.description": "The app is already running. There is no need to click multiple times to open it.",
|
||||||
"single_instance.notice.button": "OK"
|
"single_instance.notice.button": "OK",
|
||||||
|
"market.status.install_success_restart_format": "✓ Plugin '{0}' installed successfully! Please restart the application to activate it.",
|
||||||
|
"market.dialog.restart_message_format": "Plugin '{0}' has been installed successfully.\n\nTo use this plugin, you need to restart the application now.\n\nWould you like to restart?"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
"tray.menu.restart": "重启应用",
|
"tray.menu.restart": "重启应用",
|
||||||
"tray.menu.exit": "退出应用",
|
"tray.menu.exit": "退出应用",
|
||||||
"button.back_to_windows": "回到Windows",
|
"button.back_to_windows": "回到Windows",
|
||||||
|
"button.back_to_platform": "回到{0}",
|
||||||
"tooltip.back_to_windows": "回到Windows",
|
"tooltip.back_to_windows": "回到Windows",
|
||||||
|
"tooltip.back_to_platform": "回到{0}",
|
||||||
|
"platform.windows": "Windows",
|
||||||
|
"platform.linux": "Linux",
|
||||||
|
"platform.macos": "macOS",
|
||||||
"tooltip.open_settings": "设置",
|
"tooltip.open_settings": "设置",
|
||||||
"settings.title": "设置",
|
"settings.title": "设置",
|
||||||
"settings.shell.title": "设置",
|
"settings.shell.title": "设置",
|
||||||
@@ -20,7 +25,7 @@
|
|||||||
"settings.nav.group_system": "系统",
|
"settings.nav.group_system": "系统",
|
||||||
"settings.nav.group_extensions": "扩展",
|
"settings.nav.group_extensions": "扩展",
|
||||||
"settings.nav.wallpaper": "壁纸",
|
"settings.nav.wallpaper": "壁纸",
|
||||||
"settings.nav.grid": "网格",
|
"settings.nav.grid": "组件",
|
||||||
"settings.nav.color": "颜色",
|
"settings.nav.color": "颜色",
|
||||||
"settings.nav.status_bar": "状态栏",
|
"settings.nav.status_bar": "状态栏",
|
||||||
"settings.nav.weather": "天气",
|
"settings.nav.weather": "天气",
|
||||||
@@ -85,6 +90,8 @@
|
|||||||
"settings.status_bar.description": "选择顶部状态栏显示的组件。",
|
"settings.status_bar.description": "选择顶部状态栏显示的组件。",
|
||||||
"settings.status_bar.clock_header": "时间组件",
|
"settings.status_bar.clock_header": "时间组件",
|
||||||
"settings.status_bar.clock_description": "在顶部状态栏显示时钟。",
|
"settings.status_bar.clock_description": "在顶部状态栏显示时钟。",
|
||||||
|
"settings.status_bar.clock_transparent_background_label": "透明背景",
|
||||||
|
"settings.status_bar.clock_transparent_background_desc": "移除胶囊背景,仅保留时钟文字。",
|
||||||
"settings.status_bar.spacing_header": "组件间距",
|
"settings.status_bar.spacing_header": "组件间距",
|
||||||
"settings.status_bar.spacing_desc": "调整状态栏组件之间的间距。",
|
"settings.status_bar.spacing_desc": "调整状态栏组件之间的间距。",
|
||||||
"settings.status_bar.spacing_mode_compact": "紧凑",
|
"settings.status_bar.spacing_mode_compact": "紧凑",
|
||||||
@@ -294,9 +301,18 @@
|
|||||||
"settings.status_bar.clock_format_label": "时钟格式",
|
"settings.status_bar.clock_format_label": "时钟格式",
|
||||||
"settings.status_bar.clock_format.hm": "时:分",
|
"settings.status_bar.clock_format.hm": "时:分",
|
||||||
"settings.status_bar.clock_format.hms": "时:分:秒",
|
"settings.status_bar.clock_format.hms": "时:分:秒",
|
||||||
"settings.components.title": "网格",
|
"settings.components.title": "组件",
|
||||||
"settings.components.description": "调整桌面网格与布局。",
|
"settings.components.description": "调整组件布局与圆角设计。",
|
||||||
"settings.components.grid_header": "网格布局",
|
"settings.components.grid_header": "网格设置",
|
||||||
|
"settings.components.header": "网格设置",
|
||||||
|
"settings.components.short_side_label": "短边格数",
|
||||||
|
"settings.components.edge_inset_label": "屏幕边距",
|
||||||
|
"settings.components.spacing_label": "组件间距",
|
||||||
|
"settings.components.spacing_compact": "紧凑",
|
||||||
|
"settings.components.spacing_relaxed": "宽松",
|
||||||
|
"settings.components.corner_radius.header": "圆角设计",
|
||||||
|
"settings.components.corner_radius.label": "组件圆角",
|
||||||
|
"settings.components.corner_radius.description": "统一调整组件容器圆角,并随圆角增大同步扩展内部安全区。",
|
||||||
"settings.update.title": "更新",
|
"settings.update.title": "更新",
|
||||||
"settings.update.current_version_label": "当前版本",
|
"settings.update.current_version_label": "当前版本",
|
||||||
"settings.update.latest_version_label": "最新发布",
|
"settings.update.latest_version_label": "最新发布",
|
||||||
@@ -401,6 +417,7 @@
|
|||||||
"common.monet": "莫奈",
|
"common.monet": "莫奈",
|
||||||
"desktop.page_index_format": "桌面 {0}",
|
"desktop.page_index_format": "桌面 {0}",
|
||||||
"launcher.title": "应用启动台",
|
"launcher.title": "应用启动台",
|
||||||
|
"launcher.folder": "文件夹",
|
||||||
"launcher.subtitle": "按 Windows 开始菜单结构显示所有应用与文件夹",
|
"launcher.subtitle": "按 Windows 开始菜单结构显示所有应用与文件夹",
|
||||||
"launcher.subtitle_linux": "显示从 Linux .desktop 条目扫描到的已安装应用",
|
"launcher.subtitle_linux": "显示从 Linux .desktop 条目扫描到的已安装应用",
|
||||||
"launcher.empty": "未找到开始菜单条目。",
|
"launcher.empty": "未找到开始菜单条目。",
|
||||||
@@ -557,6 +574,7 @@
|
|||||||
"component_category.info": "信息推荐",
|
"component_category.info": "信息推荐",
|
||||||
"component_category.calculator": "计算器",
|
"component_category.calculator": "计算器",
|
||||||
"component_category.study": "自习",
|
"component_category.study": "自习",
|
||||||
|
"component_category.file": "文件",
|
||||||
"component.date": "日历",
|
"component.date": "日历",
|
||||||
"component.month_calendar": "月历",
|
"component.month_calendar": "月历",
|
||||||
"component.lunar_calendar": "农历",
|
"component.lunar_calendar": "农历",
|
||||||
@@ -585,6 +603,18 @@
|
|||||||
"component.blackboard_landscape": "横向小黑板",
|
"component.blackboard_landscape": "横向小黑板",
|
||||||
"component.browser": "浏览器",
|
"component.browser": "浏览器",
|
||||||
"component.office_recent_documents": "最近文档",
|
"component.office_recent_documents": "最近文档",
|
||||||
|
"whiteboard.settings.desc": "每个小黑板都会独立保存自己的笔记历史。",
|
||||||
|
"whiteboard.settings.retention.title": "笔记保留时间",
|
||||||
|
"whiteboard.settings.retention.desc": "选择这个小黑板在过期笔记被自动删除前,应当保留已保存笔记多久。",
|
||||||
|
"whiteboard.settings.retention.option": "{0} 天",
|
||||||
|
"whiteboard.settings.instance_scope": "这个保留时间设置会按每个小黑板组件实例单独存储。",
|
||||||
|
"office_recent_documents.settings.desc": "选择此小组件需要扫描的 Windows 和 Office 最近文档来源。",
|
||||||
|
"office_recent_documents.settings.sources_title": "最近文档来源",
|
||||||
|
"office_recent_documents.settings.sources_desc": "可以同时选择多个来源。勾选注册表来源时,还会保留 Office interop 的 MRU 回退。",
|
||||||
|
"office_recent_documents.settings.source.registry": "Office 注册表 MRU",
|
||||||
|
"office_recent_documents.settings.source.recent_folders": "Windows 最近文件夹",
|
||||||
|
"office_recent_documents.settings.source.jump_lists": "Windows 跳转列表",
|
||||||
|
"office_recent_documents.settings.hint": "如果关闭全部来源,此小组件会保持空白,直到再次至少启用一个来源。",
|
||||||
"component.holiday_calendar": "节假日日历",
|
"component.holiday_calendar": "节假日日历",
|
||||||
"component.study_environment": "环境",
|
"component.study_environment": "环境",
|
||||||
"component.study_session_control": "自习时段控制",
|
"component.study_session_control": "自习时段控制",
|
||||||
@@ -781,6 +811,21 @@
|
|||||||
"study.environment.value.unavailable": "--",
|
"study.environment.value.unavailable": "--",
|
||||||
"study.environment.value.display_format": "{0:F1} dB",
|
"study.environment.value.display_format": "{0:F1} dB",
|
||||||
"study.environment.value.dbfs_format": "{0:F1} dBFS",
|
"study.environment.value.dbfs_format": "{0:F1} dBFS",
|
||||||
|
"component.removable_storage": "可移动存储",
|
||||||
|
"removable_storage.settings.desc": "在桌面上显示已连接的 U 盘,并提供打开与弹出操作。",
|
||||||
|
"removable_storage.settings.behavior_title": "行为",
|
||||||
|
"removable_storage.settings.behavior_desc": "组件会自动监听可移动存储设备,并优先显示最新插入的 U 盘。",
|
||||||
|
"removable_storage.action.open": "打开",
|
||||||
|
"removable_storage.action.eject": "弹出",
|
||||||
|
"removable_storage.widget.default_name": "可移动磁盘",
|
||||||
|
"removable_storage.widget.empty_title": "未插入设备",
|
||||||
|
"removable_storage.widget.empty_subtitle": "插入 U 盘后会自动显示在这里。",
|
||||||
|
"removable_storage.widget.empty_hint": "在插入可移动设备之前,底部按钮会保持置灰不可点击。",
|
||||||
|
"removable_storage.widget.ready": "已准备好,可直接打开或弹出。",
|
||||||
|
"removable_storage.widget.ejecting": "正在弹出设备...",
|
||||||
|
"removable_storage.widget.eject_failed": "无法弹出该设备,请先关闭正在占用它的文件后再试。",
|
||||||
|
"removable_storage.widget.open_failed": "打开该设备失败。",
|
||||||
|
"removable_storage.widget.refresh_failed": "刷新可移动存储列表失败。",
|
||||||
"study.environment.settings.title": "环境组件设置",
|
"study.environment.settings.title": "环境组件设置",
|
||||||
"study.environment.settings.desc": "配置右侧实时噪音值显示内容。",
|
"study.environment.settings.desc": "配置右侧实时噪音值显示内容。",
|
||||||
"study.environment.settings.show_display_db": "显示 display dB",
|
"study.environment.settings.show_display_db": "显示 display dB",
|
||||||
@@ -890,5 +935,7 @@
|
|||||||
"placement.tile": "平铺",
|
"placement.tile": "平铺",
|
||||||
"single_instance.notice.title": "应用已经运行",
|
"single_instance.notice.title": "应用已经运行",
|
||||||
"single_instance.notice.description": "应用已经运行,无需多次点击打开。",
|
"single_instance.notice.description": "应用已经运行,无需多次点击打开。",
|
||||||
"single_instance.notice.button": "确定"
|
"single_instance.notice.button": "确定",
|
||||||
|
"market.status.install_success_restart_format": "✓ 插件'{0}'安装成功!请重启应用以激活它。",
|
||||||
|
"market.dialog.restart_message_format": "插件'{0}'已成功安装。\n\n要使用此插件,您需要立即重启应用。\n\n是否立即重启?"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using LanMountainDesktop.Settings.Core;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Models;
|
namespace LanMountainDesktop.Models;
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public bool UseSystemChrome { get; set; }
|
public bool UseSystemChrome { get; set; }
|
||||||
|
|
||||||
|
public double GlobalCornerRadiusScale { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusScale;
|
||||||
|
|
||||||
public string ThemeColorMode { get; set; } = "default_neutral";
|
public string ThemeColorMode { get; set; } = "default_neutral";
|
||||||
|
|
||||||
public string SystemMaterialMode { get; set; } = "none";
|
public string SystemMaterialMode { get; set; } = "none";
|
||||||
@@ -101,6 +104,8 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public string ClockDisplayFormat { get; set; } = "HourMinuteSecond";
|
public string ClockDisplayFormat { get; set; } = "HourMinuteSecond";
|
||||||
|
|
||||||
|
public bool StatusBarClockTransparentBackground { get; set; }
|
||||||
|
|
||||||
public string StatusBarSpacingMode { get; set; } = "Relaxed";
|
public string StatusBarSpacingMode { get; set; } = "Relaxed";
|
||||||
|
|
||||||
public int StatusBarCustomSpacingPercent { get; set; } = 12;
|
public int StatusBarCustomSpacingPercent { get; set; } = 12;
|
||||||
|
|||||||
@@ -58,12 +58,16 @@ public sealed class ComponentSettingsSnapshot
|
|||||||
|
|
||||||
public int WeatherAutoRefreshIntervalMinutes { get; set; } = 12;
|
public int WeatherAutoRefreshIntervalMinutes { get; set; } = 12;
|
||||||
|
|
||||||
|
public int WhiteboardNoteRetentionDays { get; set; } = 15;
|
||||||
|
|
||||||
public bool Stcn24ForumAutoRefreshEnabled { get; set; } = true;
|
public bool Stcn24ForumAutoRefreshEnabled { get; set; } = true;
|
||||||
|
|
||||||
public int Stcn24ForumAutoRefreshIntervalMinutes { get; set; } = 20;
|
public int Stcn24ForumAutoRefreshIntervalMinutes { get; set; } = 20;
|
||||||
|
|
||||||
public string Stcn24ForumSourceType { get; set; } = Stcn24ForumSourceTypes.LatestCreated;
|
public string Stcn24ForumSourceType { get; set; } = Stcn24ForumSourceTypes.LatestCreated;
|
||||||
|
|
||||||
|
public List<string>? OfficeRecentDocumentsEnabledSources { get; set; }
|
||||||
|
|
||||||
public ComponentSettingsSnapshot Clone()
|
public ComponentSettingsSnapshot Clone()
|
||||||
{
|
{
|
||||||
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
||||||
@@ -91,6 +95,9 @@ public sealed class ComponentSettingsSnapshot
|
|||||||
clone.WorldClockTimeZoneIds = WorldClockTimeZoneIds is { Count: > 0 }
|
clone.WorldClockTimeZoneIds = WorldClockTimeZoneIds is { Count: > 0 }
|
||||||
? new List<string>(WorldClockTimeZoneIds)
|
? new List<string>(WorldClockTimeZoneIds)
|
||||||
: [];
|
: [];
|
||||||
|
clone.OfficeRecentDocumentsEnabledSources = OfficeRecentDocumentsEnabledSources is not null
|
||||||
|
? new List<string>(OfficeRecentDocumentsEnabledSources)
|
||||||
|
: null;
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|||||||
53
LanMountainDesktop/Models/OfficeRecentDocumentSourceTypes.cs
Normal file
53
LanMountainDesktop/Models/OfficeRecentDocumentSourceTypes.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Models;
|
||||||
|
|
||||||
|
public static class OfficeRecentDocumentSourceTypes
|
||||||
|
{
|
||||||
|
public const string Registry = "registry";
|
||||||
|
public const string RecentFolders = "recent_folders";
|
||||||
|
public const string JumpLists = "jump_lists";
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> SupportedValues { get; } =
|
||||||
|
[
|
||||||
|
Registry,
|
||||||
|
RecentFolders,
|
||||||
|
JumpLists
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> DefaultValues => SupportedValues;
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> NormalizeValues(IEnumerable<string>? values, bool useDefaultWhenEmpty)
|
||||||
|
{
|
||||||
|
if (values is null)
|
||||||
|
{
|
||||||
|
return useDefaultWhenEmpty ? DefaultValues : Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = values
|
||||||
|
.Select(NormalizeValue)
|
||||||
|
.OfType<string>()
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (normalized.Length == 0 && useDefaultWhenEmpty)
|
||||||
|
{
|
||||||
|
return DefaultValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeValue(string? value)
|
||||||
|
{
|
||||||
|
return value?.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
Registry => Registry,
|
||||||
|
RecentFolders => RecentFolders,
|
||||||
|
JumpLists => JumpLists,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
LanMountainDesktop/Models/WhiteboardNoteRetentionPolicy.cs
Normal file
23
LanMountainDesktop/Models/WhiteboardNoteRetentionPolicy.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace LanMountainDesktop.Models;
|
||||||
|
|
||||||
|
public static class WhiteboardNoteRetentionPolicy
|
||||||
|
{
|
||||||
|
public const int MinimumDays = 7;
|
||||||
|
public const int MaximumDays = 15;
|
||||||
|
public const int DefaultDays = MaximumDays;
|
||||||
|
|
||||||
|
public static int NormalizeDays(int days)
|
||||||
|
{
|
||||||
|
if (days < MinimumDays)
|
||||||
|
{
|
||||||
|
return MinimumDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (days > MaximumDays)
|
||||||
|
{
|
||||||
|
return MaximumDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
LanMountainDesktop/Models/WhiteboardNoteSnapshot.cs
Normal file
60
LanMountainDesktop/Models/WhiteboardNoteSnapshot.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Models;
|
||||||
|
|
||||||
|
public sealed class WhiteboardNoteSnapshot
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
public DateTimeOffset SavedUtc { get; set; }
|
||||||
|
|
||||||
|
public List<WhiteboardStrokeSnapshot> Strokes { get; set; } = [];
|
||||||
|
|
||||||
|
public WhiteboardNoteSnapshot Clone()
|
||||||
|
{
|
||||||
|
var clone = (WhiteboardNoteSnapshot)MemberwiseClone();
|
||||||
|
clone.Strokes = Strokes is { Count: > 0 }
|
||||||
|
? new List<WhiteboardStrokeSnapshot>(Strokes.ConvertAll(stroke => stroke?.Clone() ?? new WhiteboardStrokeSnapshot()))
|
||||||
|
: [];
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WhiteboardStrokeSnapshot
|
||||||
|
{
|
||||||
|
public string Color { get; set; } = "#FF000000";
|
||||||
|
|
||||||
|
public double InkThickness { get; set; } = 2.5d;
|
||||||
|
|
||||||
|
public bool IgnorePressure { get; set; } = true;
|
||||||
|
|
||||||
|
public List<WhiteboardStylusPointSnapshot> Points { get; set; } = [];
|
||||||
|
|
||||||
|
public WhiteboardStrokeSnapshot Clone()
|
||||||
|
{
|
||||||
|
var clone = (WhiteboardStrokeSnapshot)MemberwiseClone();
|
||||||
|
clone.Points = Points is { Count: > 0 }
|
||||||
|
? new List<WhiteboardStylusPointSnapshot>(Points.ConvertAll(point => point?.Clone() ?? new WhiteboardStylusPointSnapshot()))
|
||||||
|
: [];
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WhiteboardStylusPointSnapshot
|
||||||
|
{
|
||||||
|
public double X { get; set; }
|
||||||
|
|
||||||
|
public double Y { get; set; }
|
||||||
|
|
||||||
|
public double Pressure { get; set; } = 0.5d;
|
||||||
|
|
||||||
|
public double Width { get; set; }
|
||||||
|
|
||||||
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
public WhiteboardStylusPointSnapshot Clone()
|
||||||
|
{
|
||||||
|
return (WhiteboardStylusPointSnapshot)MemberwiseClone();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.WebView.Desktop;
|
using Avalonia.WebView.Desktop;
|
||||||
|
using LanMountainDesktop.DesktopHost;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
@@ -20,9 +21,11 @@ sealed class Program
|
|||||||
{
|
{
|
||||||
AppLogger.Initialize();
|
AppLogger.Initialize();
|
||||||
RegisterGlobalExceptionLogging();
|
RegisterGlobalExceptionLogging();
|
||||||
InitializeDeviceId();
|
DesktopBootstrap.InitializeStartupServices(
|
||||||
InitializeCrashReporting();
|
InitializeDeviceId,
|
||||||
InitializeUserBehaviorAnalytics();
|
InitializeCrashReporting,
|
||||||
|
InitializeUserBehaviorAnalytics,
|
||||||
|
ScheduleWhiteboardNoteStartupCleanup);
|
||||||
var restartParentProcessId = AppRestartService.TryGetRestartParentProcessId(args);
|
var restartParentProcessId = AppRestartService.TryGetRestartParentProcessId(args);
|
||||||
|
|
||||||
using var singleInstance = AcquireSingleInstance(restartParentProcessId);
|
using var singleInstance = AcquireSingleInstance(restartParentProcessId);
|
||||||
@@ -88,6 +91,25 @@ sealed class Program
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ScheduleWhiteboardNoteStartupCleanup()
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deletedCount = new WhiteboardNotePersistenceService().DeleteExpiredNotesBatch(batchSize: 512);
|
||||||
|
if (deletedCount > 0)
|
||||||
|
{
|
||||||
|
AppLogger.Info("Startup", $"Deleted {deletedCount} expired whiteboard notes during startup maintenance.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("Startup", "Failed to run whiteboard note startup maintenance.", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static SingleInstanceService AcquireSingleInstance(int? restartParentProcessId)
|
private static SingleInstanceService AcquireSingleInstance(int? restartParentProcessId)
|
||||||
{
|
{
|
||||||
var singleInstance = SingleInstanceService.CreateDefault();
|
var singleInstance = SingleInstanceService.CreateDefault();
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ public sealed class AppDatabaseService
|
|||||||
_databasePath = Path.Combine(dataDirectory, "app.db");
|
_databasePath = Path.Combine(dataDirectory, "app.db");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AppDatabaseService(string databasePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(databasePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Database path cannot be null or whitespace.", nameof(databasePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
_databasePath = databasePath;
|
||||||
|
}
|
||||||
|
|
||||||
public SqliteConnection OpenConnection()
|
public SqliteConnection OpenConnection()
|
||||||
{
|
{
|
||||||
var directory = Path.GetDirectoryName(_databasePath);
|
var directory = Path.GetDirectoryName(_databasePath);
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ using Avalonia.Media;
|
|||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using LanMountainDesktop.Appearance;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
|
using LanMountainDesktop.Settings.Core;
|
||||||
|
using LanMountainDesktop.Shared.Contracts;
|
||||||
using LanMountainDesktop.Theme;
|
using LanMountainDesktop.Theme;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
@@ -41,6 +44,8 @@ public sealed record AppearanceThemeSnapshot(
|
|||||||
string ThemeColorMode,
|
string ThemeColorMode,
|
||||||
string? UserThemeColor,
|
string? UserThemeColor,
|
||||||
string? SelectedWallpaperSeed,
|
string? SelectedWallpaperSeed,
|
||||||
|
double GlobalCornerRadiusScale,
|
||||||
|
AppearanceCornerRadiusTokens CornerRadiusTokens,
|
||||||
string ResolvedSeedSource,
|
string ResolvedSeedSource,
|
||||||
MonetPalette MonetPalette,
|
MonetPalette MonetPalette,
|
||||||
Color AccentColor,
|
Color AccentColor,
|
||||||
@@ -464,6 +469,13 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
|||||||
var context = CreateThemeContext(snapshot);
|
var context = CreateThemeContext(snapshot);
|
||||||
ThemeColorSystemService.ApplyThemeResources(resources, context);
|
ThemeColorSystemService.ApplyThemeResources(resources, context);
|
||||||
GlassEffectService.ApplyGlassResources(resources, context);
|
GlassEffectService.ApplyGlassResources(resources, context);
|
||||||
|
resources["DesignCornerRadiusMicro"] = snapshot.CornerRadiusTokens.Micro;
|
||||||
|
resources["DesignCornerRadiusXs"] = snapshot.CornerRadiusTokens.Xs;
|
||||||
|
resources["DesignCornerRadiusSm"] = snapshot.CornerRadiusTokens.Sm;
|
||||||
|
resources["DesignCornerRadiusMd"] = snapshot.CornerRadiusTokens.Md;
|
||||||
|
resources["DesignCornerRadiusLg"] = snapshot.CornerRadiusTokens.Lg;
|
||||||
|
resources["DesignCornerRadiusXl"] = snapshot.CornerRadiusTokens.Xl;
|
||||||
|
resources["DesignCornerRadiusIsland"] = snapshot.CornerRadiusTokens.Island;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AppearanceMaterialSurface GetMaterialSurface(MaterialSurfaceRole role)
|
public AppearanceMaterialSurface GetMaterialSurface(MaterialSurfaceRole role)
|
||||||
@@ -538,6 +550,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
|||||||
if (!refreshAll &&
|
if (!refreshAll &&
|
||||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) &&
|
!changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) &&
|
||||||
!changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) &&
|
!changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) &&
|
||||||
|
!changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) &&
|
||||||
!(respondsToThemeColor &&
|
!(respondsToThemeColor &&
|
||||||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) &&
|
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) &&
|
||||||
!(respondsToWallpaper &&
|
!(respondsToWallpaper &&
|
||||||
@@ -559,6 +572,8 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
|||||||
bool queueWallpaperPaletteBuild)
|
bool queueWallpaperPaletteBuild)
|
||||||
{
|
{
|
||||||
var availableModes = _windowMaterialService.GetAvailableModes();
|
var availableModes = _windowMaterialService.GetAvailableModes();
|
||||||
|
var globalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(themeState.GlobalCornerRadiusScale);
|
||||||
|
var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(globalCornerRadiusScale);
|
||||||
MonetPalette palette;
|
MonetPalette palette;
|
||||||
IReadOnlyList<Color> wallpaperSeedCandidates;
|
IReadOnlyList<Color> wallpaperSeedCandidates;
|
||||||
Color effectiveSeedColor;
|
Color effectiveSeedColor;
|
||||||
@@ -598,6 +613,8 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
|||||||
themeColorMode,
|
themeColorMode,
|
||||||
themeState.ThemeColor,
|
themeState.ThemeColor,
|
||||||
selectedWallpaperSeed,
|
selectedWallpaperSeed,
|
||||||
|
globalCornerRadiusScale,
|
||||||
|
cornerRadiusTokens,
|
||||||
resolvedSeedSource,
|
resolvedSeedSource,
|
||||||
palette,
|
palette,
|
||||||
ResolveAccentColor(themeColorMode, themeState.ThemeColor, palette),
|
ResolveAccentColor(themeColorMode, themeState.ThemeColor, palette),
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
|
|||||||
|
|
||||||
public void DeleteForComponent(string componentId, string? placementId)
|
public void DeleteForComponent(string componentId, string? placementId)
|
||||||
{
|
{
|
||||||
|
_ = new WhiteboardNotePersistenceService().DeleteNote(componentId, placementId);
|
||||||
|
|
||||||
if (_settingsService is not null)
|
if (_settingsService is not null)
|
||||||
{
|
{
|
||||||
_settingsService.SaveSnapshot(
|
_settingsService.SaveSnapshot(
|
||||||
|
|||||||
@@ -72,6 +72,18 @@ public static class DesktopComponentEditorRegistryFactory
|
|||||||
[BuiltInComponentIds.DesktopStudyEnvironment] = new(
|
[BuiltInComponentIds.DesktopStudyEnvironment] = new(
|
||||||
BuiltInComponentIds.DesktopStudyEnvironment,
|
BuiltInComponentIds.DesktopStudyEnvironment,
|
||||||
context => new StudyEnvironmentComponentEditor(context)),
|
context => new StudyEnvironmentComponentEditor(context)),
|
||||||
|
[BuiltInComponentIds.DesktopRemovableStorage] = new(
|
||||||
|
BuiltInComponentIds.DesktopRemovableStorage,
|
||||||
|
context => new RemovableStorageComponentEditor(context)),
|
||||||
|
[BuiltInComponentIds.DesktopWhiteboard] = new(
|
||||||
|
BuiltInComponentIds.DesktopWhiteboard,
|
||||||
|
context => new WhiteboardComponentEditor(context)),
|
||||||
|
[BuiltInComponentIds.DesktopBlackboardLandscape] = new(
|
||||||
|
BuiltInComponentIds.DesktopBlackboardLandscape,
|
||||||
|
context => new WhiteboardComponentEditor(context)),
|
||||||
|
[BuiltInComponentIds.DesktopOfficeRecentDocuments] = new(
|
||||||
|
BuiltInComponentIds.DesktopOfficeRecentDocuments,
|
||||||
|
context => new OfficeRecentDocumentsComponentEditor(context)),
|
||||||
[BuiltInComponentIds.DesktopWeather] = CreateWeatherRegistration(BuiltInComponentIds.DesktopWeather),
|
[BuiltInComponentIds.DesktopWeather] = CreateWeatherRegistration(BuiltInComponentIds.DesktopWeather),
|
||||||
[BuiltInComponentIds.DesktopWeatherClock] = CreateWeatherRegistration(BuiltInComponentIds.DesktopWeatherClock),
|
[BuiltInComponentIds.DesktopWeatherClock] = CreateWeatherRegistration(BuiltInComponentIds.DesktopWeatherClock),
|
||||||
[BuiltInComponentIds.DesktopHourlyWeather] = CreateWeatherRegistration(BuiltInComponentIds.DesktopHourlyWeather),
|
[BuiltInComponentIds.DesktopHourlyWeather] = CreateWeatherRegistration(BuiltInComponentIds.DesktopHourlyWeather),
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ public static class DesktopComponentRegistryFactory
|
|||||||
var pluginSettings = new PluginScopedSettingsService(
|
var pluginSettings = new PluginScopedSettingsService(
|
||||||
contribution.Plugin.Manifest.Id,
|
contribution.Plugin.Manifest.Id,
|
||||||
settingsService);
|
settingsService);
|
||||||
|
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
|
||||||
var pluginContext = new PluginDesktopComponentContext(
|
var pluginContext = new PluginDesktopComponentContext(
|
||||||
contribution.Plugin.Manifest,
|
contribution.Plugin.Manifest,
|
||||||
contribution.Plugin.Context.PluginDirectory,
|
contribution.Plugin.Context.PluginDirectory,
|
||||||
@@ -131,6 +132,8 @@ public static class DesktopComponentRegistryFactory
|
|||||||
contribution.Registration.ComponentId,
|
contribution.Registration.ComponentId,
|
||||||
context.PlacementId,
|
context.PlacementId,
|
||||||
context.CellSize,
|
context.CellSize,
|
||||||
|
appearanceSnapshot.GlobalCornerRadiusScale,
|
||||||
|
appearanceSnapshot.CornerRadiusTokens,
|
||||||
pluginSettings);
|
pluginSettings);
|
||||||
|
|
||||||
return contribution.Registration.ControlFactory(contribution.Plugin.Services, pluginContext);
|
return contribution.Registration.ControlFactory(contribution.Plugin.Services, pluginContext);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public sealed record ComponentLibraryCategoryEntry(
|
|||||||
|
|
||||||
public sealed record ComponentLibraryCreateContext(
|
public sealed record ComponentLibraryCreateContext(
|
||||||
double CellSize,
|
double CellSize,
|
||||||
|
double GlobalCornerRadiusScale,
|
||||||
TimeZoneService TimeZoneService,
|
TimeZoneService TimeZoneService,
|
||||||
IWeatherInfoService WeatherInfoService,
|
IWeatherInfoService WeatherInfoService,
|
||||||
IRecommendationInfoService RecommendationInfoService,
|
IRecommendationInfoService RecommendationInfoService,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
public interface IWhiteboardNotePersistenceService
|
||||||
|
{
|
||||||
|
WhiteboardNoteSnapshot LoadNote(string componentId, string? placementId, int retentionDays);
|
||||||
|
|
||||||
|
void SaveNote(string componentId, string? placementId, WhiteboardNoteSnapshot snapshot, int retentionDays);
|
||||||
|
|
||||||
|
bool DeleteNote(string componentId, string? placementId);
|
||||||
|
|
||||||
|
bool TryDeleteExpiredNote(string componentId, string? placementId, int retentionDays);
|
||||||
|
|
||||||
|
bool IsExpired(WhiteboardNoteSnapshot snapshot, int retentionDays, DateTimeOffset? now = null);
|
||||||
|
|
||||||
|
DateTimeOffset? GetExpirationUtc(WhiteboardNoteSnapshot snapshot, int retentionDays);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services;
|
namespace LanMountainDesktop.Services;
|
||||||
@@ -16,6 +17,23 @@ public sealed class LocalizationService
|
|||||||
private readonly Dictionary<string, Dictionary<string, string>> _cache =
|
private readonly Dictionary<string, Dictionary<string, string>> _cache =
|
||||||
new(StringComparer.OrdinalIgnoreCase);
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除指定语言代码的缓存,强制下次重新加载。
|
||||||
|
/// 在语言切换时调用此方法以确保加载最新的语言文件。
|
||||||
|
/// </summary>
|
||||||
|
public void ClearCache(string? languageCode = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(languageCode))
|
||||||
|
{
|
||||||
|
_cache.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var normalizedCode = NormalizeLanguageCode(languageCode);
|
||||||
|
_cache.Remove(normalizedCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string NormalizeLanguageCode(string? languageCode)
|
public string NormalizeLanguageCode(string? languageCode)
|
||||||
{
|
{
|
||||||
return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase)
|
return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -42,14 +60,17 @@ public sealed class LocalizationService
|
|||||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var filePath = Path.Combine(AppContext.BaseDirectory, "Localization", $"{languageCode}.json");
|
var json = TryLoadFromFileSystem(languageCode);
|
||||||
if (File.Exists(filePath))
|
if (string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
json = TryLoadFromEmbeddedResource(languageCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(json))
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(filePath);
|
|
||||||
// Defensive: tolerate accidentally duplicated UTF-8 BOM characters at file start.
|
|
||||||
json = json.TrimStart('\uFEFF');
|
json = json.TrimStart('\uFEFF');
|
||||||
var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions);
|
var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions);
|
||||||
if (data is not null)
|
if (data is not null && data.Count > 0)
|
||||||
{
|
{
|
||||||
result = new Dictionary<string, string>(data, StringComparer.OrdinalIgnoreCase);
|
result = new Dictionary<string, string>(data, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
@@ -60,7 +81,48 @@ public sealed class LocalizationService
|
|||||||
// Keep empty table for resilience.
|
// Keep empty table for resilience.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有当语言表非空时才缓存,这样如果加载失败可以下次重试
|
||||||
|
if (result.Count > 0)
|
||||||
|
{
|
||||||
_cache[languageCode] = result;
|
_cache[languageCode] = result;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string? TryLoadFromFileSystem(string languageCode)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(AppContext.BaseDirectory, "Localization", $"{languageCode}.json");
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return File.ReadAllText(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Continue to next method
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? TryLoadFromEmbeddedResource(string languageCode)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var resourceName = $"LanMountainDesktop.Localization.{languageCode}.json";
|
||||||
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
|
if (stream != null)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Continue to next method
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.RegularExpressions;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using System.Threading;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using MudTools.OfficeInterop;
|
||||||
|
using MudTools.OfficeInterop.Excel;
|
||||||
|
using MudTools.OfficeInterop.Word;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services;
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
public interface IOfficeRecentDocumentsService
|
public interface IOfficeRecentDocumentsService
|
||||||
{
|
{
|
||||||
List<OfficeRecentDocument> GetRecentDocuments(int maxCount = 20);
|
List<OfficeRecentDocument> GetRecentDocuments(int maxCount = 20, IReadOnlyCollection<string>? enabledSources = null);
|
||||||
void OpenDocument(string filePath);
|
void OpenDocument(string filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,78 +31,67 @@ public sealed class OfficeRecentDocument
|
|||||||
public DateTime LastModifiedTime { get; set; }
|
public DateTime LastModifiedTime { get; set; }
|
||||||
public long FileSizeBytes { get; set; }
|
public long FileSizeBytes { get; set; }
|
||||||
public string IconGlyph { get; set; } = string.Empty;
|
public string IconGlyph { get; set; } = string.Empty;
|
||||||
|
internal DateTime? RecentAccessTime { get; set; }
|
||||||
|
internal int SourcePriority { get; set; }
|
||||||
|
internal int SourceOrder { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService
|
public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService
|
||||||
{
|
{
|
||||||
|
private const string LogCategory = "OfficeRecentDocs";
|
||||||
private static readonly string[] OfficeExtensions = { ".doc", ".docx", ".dot", ".dotx", ".rtf" };
|
private static readonly string[] OfficeExtensions = { ".doc", ".docx", ".dot", ".dotx", ".rtf" };
|
||||||
private static readonly string[] ExcelExtensions = { ".xls", ".xlsx", ".xlsm", ".xlsb", ".csv" };
|
private static readonly string[] ExcelExtensions = { ".xls", ".xlsx", ".xlsm", ".xlsb", ".csv" };
|
||||||
private static readonly string[] PowerPointExtensions = { ".ppt", ".pptx", ".pptm", ".pps", ".ppsx" };
|
private static readonly string[] PowerPointExtensions = { ".ppt", ".pptx", ".pptm", ".pps", ".ppsx" };
|
||||||
|
private static readonly Regex OfficeFilePathRegex = new(
|
||||||
|
@"(?:[A-Z]:\\|\\\\)[^\x00-\x1F""<>|]+?\.(?:docx?|dotx?|rtf|xlsx?|xlsm|xlsb|csv|pptx?|pptm|ppsx?)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
private static readonly Regex OfficeMruTimestampRegex = new(
|
||||||
|
@"\[T(?<filetime>[0-9A-F]+)\]",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
public List<OfficeRecentDocument> GetRecentDocuments(int maxCount = 20)
|
public List<OfficeRecentDocument> GetRecentDocuments(int maxCount = 20, IReadOnlyCollection<string>? enabledSources = null)
|
||||||
{
|
{
|
||||||
var documents = new List<OfficeRecentDocument>();
|
var documents = new List<OfficeRecentDocument>();
|
||||||
var recentPaths = GetRecentFolders();
|
var normalizedSources = OfficeRecentDocumentSourceTypes.NormalizeValues(
|
||||||
|
enabledSources,
|
||||||
|
useDefaultWhenEmpty: enabledSources is null);
|
||||||
|
|
||||||
foreach (var recentPath in recentPaths)
|
if (!OperatingSystem.IsWindows() || normalizedSources.Count == 0)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(recentPath))
|
return documents;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
var useRegistry = normalizedSources.Contains(OfficeRecentDocumentSourceTypes.Registry, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var useRecentFolders = normalizedSources.Contains(OfficeRecentDocumentSourceTypes.RecentFolders, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var useJumpLists = normalizedSources.Contains(OfficeRecentDocumentSourceTypes.JumpLists, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (useRegistry)
|
||||||
{
|
{
|
||||||
var files = Directory.GetFiles(recentPath, "*.lnk");
|
TryGetFromRegistry(documents);
|
||||||
foreach (var lnkPath in files)
|
|
||||||
{
|
|
||||||
var targetPath = GetShortcutTarget(lnkPath);
|
|
||||||
if (string.IsNullOrEmpty(targetPath))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension = Path.GetExtension(targetPath).ToLowerInvariant();
|
if (useRecentFolders)
|
||||||
if (!IsOfficeFile(extension))
|
|
||||||
{
|
{
|
||||||
continue;
|
TryGetFromRecentFolders(documents);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!System.IO.File.Exists(targetPath))
|
if (useJumpLists)
|
||||||
{
|
{
|
||||||
continue;
|
TryGetFromJumpLists(documents);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
if (useRegistry && documents.Count < maxCount)
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(targetPath);
|
TryGetFromMudToolsInterop(documents);
|
||||||
var doc = new OfficeRecentDocument
|
|
||||||
{
|
|
||||||
FileName = Path.GetFileNameWithoutExtension(targetPath),
|
|
||||||
FilePath = targetPath,
|
|
||||||
Extension = extension,
|
|
||||||
LastModifiedTime = fileInfo.LastWriteTime,
|
|
||||||
FileSizeBytes = fileInfo.Length,
|
|
||||||
IconGlyph = GetIconGlyph(extension)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!documents.Any(d => d.FilePath == targetPath))
|
|
||||||
{
|
|
||||||
documents.Add(doc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return documents
|
return documents
|
||||||
.OrderByDescending(d => d.LastModifiedTime)
|
.GroupBy(d => d.FilePath, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(MergeDocuments)
|
||||||
|
.OrderByDescending(d => d.RecentAccessTime ?? DateTime.MinValue)
|
||||||
|
.ThenBy(d => d.SourcePriority)
|
||||||
|
.ThenBy(d => d.SourceOrder)
|
||||||
|
.ThenByDescending(d => d.LastModifiedTime)
|
||||||
.Take(maxCount)
|
.Take(maxCount)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -109,30 +105,587 @@ public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService
|
|||||||
FileName = filePath,
|
FileName = filePath,
|
||||||
UseShellExecute = true
|
UseShellExecute = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Process.Start(startInfo);
|
Process.Start(startInfo);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(LogCategory, $"Failed to open Office document '{filePath}'.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OfficeRecentDocument MergeDocuments(IGrouping<string, OfficeRecentDocument> group)
|
||||||
|
{
|
||||||
|
var preferred = group
|
||||||
|
.OrderByDescending(d => d.RecentAccessTime ?? DateTime.MinValue)
|
||||||
|
.ThenBy(d => d.SourcePriority)
|
||||||
|
.ThenBy(d => d.SourceOrder)
|
||||||
|
.ThenByDescending(d => d.LastModifiedTime)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
return new OfficeRecentDocument
|
||||||
|
{
|
||||||
|
FileName = preferred.FileName,
|
||||||
|
FilePath = preferred.FilePath,
|
||||||
|
Extension = preferred.Extension,
|
||||||
|
LastModifiedTime = group.Max(d => d.LastModifiedTime),
|
||||||
|
FileSizeBytes = preferred.FileSizeBytes,
|
||||||
|
IconGlyph = preferred.IconGlyph,
|
||||||
|
RecentAccessTime = group
|
||||||
|
.Where(d => d.RecentAccessTime.HasValue)
|
||||||
|
.Select(d => d.RecentAccessTime)
|
||||||
|
.Max(),
|
||||||
|
SourcePriority = preferred.SourcePriority,
|
||||||
|
SourceOrder = preferred.SourceOrder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private void TryGetFromMudToolsInterop(List<OfficeRecentDocument> documents)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RunOnStaThread(() =>
|
||||||
|
{
|
||||||
|
var sourceOrder = 0;
|
||||||
|
TryGetFromWordInterop(documents, ref sourceOrder);
|
||||||
|
TryGetFromExcelInterop(documents, ref sourceOrder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(LogCategory, "MudTools.OfficeInterop recent-document read failed.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private void TryGetFromWordInterop(List<OfficeRecentDocument> documents, ref int sourceOrder)
|
||||||
|
{
|
||||||
|
if (!TryGetOfficeApplication("Word.Application", out var comObject, out var createdNew))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object? application = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
application = WordFactory.Connection(comObject!);
|
||||||
|
|
||||||
|
if (createdNew)
|
||||||
|
{
|
||||||
|
TrySetProperty(comObject, "Visible", false);
|
||||||
|
TrySetProperty(application, "DisplayAlerts", WdAlertLevel.wdAlertsNone);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddInteropRecentFiles(documents, GetPropertyValue(application, "RecentFiles"), 0, ref sourceOrder);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(LogCategory, "Failed to read Word recent files via MudTools.OfficeInterop.", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CleanupOfficeApplication(application, comObject, createdNew);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private void TryGetFromExcelInterop(List<OfficeRecentDocument> documents, ref int sourceOrder)
|
||||||
|
{
|
||||||
|
if (!TryGetOfficeApplication("Excel.Application", out var comObject, out var createdNew))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object? application = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
application = ExcelFactory.Connection(comObject!);
|
||||||
|
|
||||||
|
if (createdNew)
|
||||||
|
{
|
||||||
|
TrySetProperty(comObject, "Visible", false);
|
||||||
|
TrySetProperty(application, "DisplayAlerts", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddInteropRecentFiles(documents, GetPropertyValue(application, "RecentFiles"), 0, ref sourceOrder);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(LogCategory, "Failed to read Excel recent files via MudTools.OfficeInterop.", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CleanupOfficeApplication(application, comObject, createdNew);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddInteropRecentFiles(
|
||||||
|
List<OfficeRecentDocument> documents,
|
||||||
|
object? recentFiles,
|
||||||
|
int sourcePriority,
|
||||||
|
ref int sourceOrder)
|
||||||
|
{
|
||||||
|
if (recentFiles == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = GetIntProperty(recentFiles, "Count");
|
||||||
|
var itemProperty = recentFiles.GetType().GetProperty("Item");
|
||||||
|
if (count <= 0 || itemProperty == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 1; index <= count; index++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var recentFile = itemProperty.GetValue(recentFiles, new object[] { index });
|
||||||
|
var filePath = GetStringProperty(recentFile, "Path");
|
||||||
|
AddDocumentIfExists(documents, filePath, sourcePriority, sourceOrder++, null);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore a single malformed MRU entry and keep processing the rest.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private static bool TryGetOfficeApplication(string progId, out object? comObject, out bool createdNew)
|
||||||
|
{
|
||||||
|
comObject = null;
|
||||||
|
createdNew = false;
|
||||||
|
|
||||||
|
var applicationType = Type.GetTypeFromProgID(progId, throwOnError: false);
|
||||||
|
if (applicationType == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
comObject = Activator.CreateInstance(applicationType);
|
||||||
|
createdNew = comObject != null;
|
||||||
|
return comObject != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private static void CleanupOfficeApplication(object? application, object? comObject, bool createdNew)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (createdNew && application != null)
|
||||||
|
{
|
||||||
|
InvokeParameterlessMethod(application, "Quit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (application is IDisposable disposable)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseComObject(application);
|
||||||
|
if (!ReferenceEquals(application, comObject))
|
||||||
|
{
|
||||||
|
ReleaseComObject(comObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private static void ReleaseComObject(object? instance)
|
||||||
|
{
|
||||||
|
if (instance == null || !Marshal.IsComObject(instance))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.FinalReleaseComObject(instance);
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<string> GetRecentFolders()
|
[SupportedOSPlatform("windows")]
|
||||||
|
private static void RunOnStaThread(Action action)
|
||||||
{
|
{
|
||||||
var folders = new List<string>();
|
Exception? exception = null;
|
||||||
|
using var finished = new ManualResetEventSlim();
|
||||||
|
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
exception = ex;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
finished.Set();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
thread.IsBackground = true;
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
finished.Wait();
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to run Office interop on STA thread.", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private void TryGetFromRegistry(List<OfficeRecentDocument> documents)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var officeRoot = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Office");
|
||||||
|
if (officeRoot == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var versions = officeRoot
|
||||||
|
.GetSubKeyNames()
|
||||||
|
.Where(IsOfficeVersionKey)
|
||||||
|
.OrderByDescending(ParseVersionKey)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var sourceOrder = 0;
|
||||||
|
foreach (var version in versions)
|
||||||
|
{
|
||||||
|
TryGetFromRegistryApp(documents, version, "Word", ref sourceOrder);
|
||||||
|
TryGetFromRegistryApp(documents, version, "Excel", ref sourceOrder);
|
||||||
|
TryGetFromRegistryApp(documents, version, "PowerPoint", ref sourceOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(LogCategory, "Failed to read Office MRU entries from the registry.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private void TryGetFromRegistryApp(List<OfficeRecentDocument> documents, string version, string appName, ref int sourceOrder)
|
||||||
|
{
|
||||||
|
TryGetFromRegistryMruKey(documents, $@"Software\Microsoft\Office\{version}\{appName}\File MRU", ref sourceOrder);
|
||||||
|
|
||||||
|
using var userMruRoot = Registry.CurrentUser.OpenSubKey($@"Software\Microsoft\Office\{version}\{appName}\User MRU");
|
||||||
|
if (userMruRoot == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var identityKey in userMruRoot.GetSubKeyNames())
|
||||||
|
{
|
||||||
|
TryGetFromRegistryMruKey(
|
||||||
|
documents,
|
||||||
|
$@"Software\Microsoft\Office\{version}\{appName}\User MRU\{identityKey}\File MRU",
|
||||||
|
ref sourceOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private void TryGetFromRegistryMruKey(List<OfficeRecentDocument> documents, string registryPath, ref int sourceOrder)
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(registryPath);
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = key
|
||||||
|
.GetValueNames()
|
||||||
|
.Where(name => name.StartsWith("Item ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(name => new
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Order = ParseMruItemOrder(name),
|
||||||
|
Value = key.GetValue(name) as string
|
||||||
|
})
|
||||||
|
.Where(entry => !string.IsNullOrWhiteSpace(entry.Value))
|
||||||
|
.OrderBy(entry => entry.Order);
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
var (filePath, recentAccessTime) = ParseOfficeMruValue(entry.Value!);
|
||||||
|
AddDocumentIfExists(documents, filePath, 1, sourceOrder++, recentAccessTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryGetFromRecentFolders(List<OfficeRecentDocument> documents)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var linkFiles = GetRecentFolders()
|
||||||
|
.Where(Directory.Exists)
|
||||||
|
.SelectMany(path => Directory.EnumerateFiles(path, "*.lnk"))
|
||||||
|
.Select(path => new FileInfo(path))
|
||||||
|
.OrderByDescending(info => info.LastWriteTimeUtc)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var sourceOrder = 0;
|
||||||
|
foreach (var linkFile in linkFiles)
|
||||||
|
{
|
||||||
|
var targetPath = GetShortcutTarget(linkFile.FullName);
|
||||||
|
AddDocumentIfExists(documents, targetPath, 2, sourceOrder++, linkFile.LastWriteTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(LogCategory, "Failed to read Windows Recent shortcut folders.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryGetFromJumpLists(List<OfficeRecentDocument> documents)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jumpListFiles = GetJumpListFolders()
|
||||||
|
.Where(Directory.Exists)
|
||||||
|
.SelectMany(path => Directory.EnumerateFiles(path, "*.automaticDestinations-ms")
|
||||||
|
.Concat(Directory.EnumerateFiles(path, "*.customDestinations-ms")))
|
||||||
|
.Select(path => new FileInfo(path))
|
||||||
|
.OrderByDescending(info => info.LastWriteTimeUtc)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var sourceOrder = 0;
|
||||||
|
foreach (var jumpListFile in jumpListFiles)
|
||||||
|
{
|
||||||
|
TryParseJumpListFile(jumpListFile, documents, ref sourceOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(LogCategory, "Failed to read Windows Jump Lists for Office documents.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryParseJumpListFile(FileInfo jumpListFile, List<OfficeRecentDocument> documents, ref int sourceOrder)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = File.ReadAllBytes(jumpListFile.FullName);
|
||||||
|
foreach (var filePath in ExtractPossiblePaths(bytes))
|
||||||
|
{
|
||||||
|
AddDocumentIfExists(documents, filePath, 3, sourceOrder++, jumpListFile.LastWriteTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore a single Jump List file and keep scanning the rest.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ExtractPossiblePaths(byte[] bytes)
|
||||||
|
{
|
||||||
|
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var text in new[]
|
||||||
|
{
|
||||||
|
Encoding.Unicode.GetString(bytes),
|
||||||
|
Encoding.Latin1.GetString(bytes)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
foreach (Match match in OfficeFilePathRegex.Matches(text))
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizeFilePath(match.Value);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedPath))
|
||||||
|
{
|
||||||
|
paths.Add(normalizedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDocumentIfExists(
|
||||||
|
List<OfficeRecentDocument> documents,
|
||||||
|
string? filePath,
|
||||||
|
int sourcePriority,
|
||||||
|
int sourceOrder,
|
||||||
|
DateTime? recentAccessTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizeFilePath(filePath);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(normalizedPath).ToLowerInvariant();
|
||||||
|
if (!IsOfficeFile(extension) || !File.Exists(normalizedPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfo = new FileInfo(normalizedPath);
|
||||||
|
documents.Add(new OfficeRecentDocument
|
||||||
|
{
|
||||||
|
FileName = Path.GetFileNameWithoutExtension(normalizedPath),
|
||||||
|
FilePath = normalizedPath,
|
||||||
|
Extension = extension,
|
||||||
|
LastModifiedTime = fileInfo.LastWriteTime,
|
||||||
|
FileSizeBytes = fileInfo.Length,
|
||||||
|
IconGlyph = GetIconGlyph(extension),
|
||||||
|
RecentAccessTime = recentAccessTime,
|
||||||
|
SourcePriority = sourcePriority,
|
||||||
|
SourceOrder = sourceOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore a single file and keep processing the rest of the MRU list.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetRecentFolders()
|
||||||
|
{
|
||||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
folders.Add(Path.Combine(appData, "Microsoft", "Word", "Recent"));
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
folders.Add(Path.Combine(appData, "Microsoft", "Excel", "Recent"));
|
|
||||||
folders.Add(Path.Combine(appData, "Microsoft", "PowerPoint", "Recent"));
|
|
||||||
|
|
||||||
return folders;
|
return new[]
|
||||||
|
{
|
||||||
|
Path.Combine(appData, "Microsoft", "Windows", "Recent"),
|
||||||
|
Path.Combine(appData, "Microsoft", "Word", "Recent"),
|
||||||
|
Path.Combine(appData, "Microsoft", "Excel", "Recent"),
|
||||||
|
Path.Combine(appData, "Microsoft", "PowerPoint", "Recent"),
|
||||||
|
Path.Combine(localAppData, "Microsoft", "Office", "Word", "Recent"),
|
||||||
|
Path.Combine(localAppData, "Microsoft", "Office", "Excel", "Recent"),
|
||||||
|
Path.Combine(localAppData, "Microsoft", "Office", "PowerPoint", "Recent")
|
||||||
|
}.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetJumpListFolders()
|
||||||
|
{
|
||||||
|
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
Path.Combine(appData, "Microsoft", "Windows", "Recent", "AutomaticDestinations"),
|
||||||
|
Path.Combine(appData, "Microsoft", "Windows", "Recent", "CustomDestinations"),
|
||||||
|
Path.Combine(localAppData, "Microsoft", "Windows", "Recent", "AutomaticDestinations"),
|
||||||
|
Path.Combine(localAppData, "Microsoft", "Windows", "Recent", "CustomDestinations")
|
||||||
|
}.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsOfficeVersionKey(string keyName)
|
||||||
|
{
|
||||||
|
return Version.TryParse(keyName, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Version ParseVersionKey(string keyName)
|
||||||
|
{
|
||||||
|
return Version.TryParse(keyName, out var version) ? version : new Version(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ParseMruItemOrder(string valueName)
|
||||||
|
{
|
||||||
|
var numberText = valueName["Item ".Length..];
|
||||||
|
return int.TryParse(numberText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var number)
|
||||||
|
? number
|
||||||
|
: int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string? FilePath, DateTime? RecentAccessTime) ParseOfficeMruValue(string rawValue)
|
||||||
|
{
|
||||||
|
var filePath = ExtractOfficeFilePath(rawValue);
|
||||||
|
DateTime? recentAccessTime = null;
|
||||||
|
|
||||||
|
var timestampMatch = OfficeMruTimestampRegex.Match(rawValue);
|
||||||
|
if (timestampMatch.Success &&
|
||||||
|
long.TryParse(timestampMatch.Groups["filetime"].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var fileTime) &&
|
||||||
|
fileTime > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
recentAccessTime = DateTime.FromFileTimeUtc(fileTime).ToLocalTime();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
recentAccessTime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (filePath, recentAccessTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractOfficeFilePath(string rawValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawValue))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var markerIndex = rawValue.LastIndexOf('*');
|
||||||
|
var candidate = markerIndex >= 0
|
||||||
|
? rawValue[(markerIndex + 1)..]
|
||||||
|
: rawValue;
|
||||||
|
|
||||||
|
var normalizedCandidate = NormalizeFilePath(candidate);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedCandidate) && IsOfficeFile(Path.GetExtension(normalizedCandidate)))
|
||||||
|
{
|
||||||
|
return normalizedCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = OfficeFilePathRegex.Match(rawValue);
|
||||||
|
return match.Success ? NormalizeFilePath(match.Value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeFilePath(string? rawPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = rawPath.Trim('\0', ' ', '"');
|
||||||
|
candidate = Environment.ExpandEnvironmentVariables(candidate);
|
||||||
|
|
||||||
|
if (Uri.TryCreate(candidate, UriKind.Absolute, out var uri) && uri.IsFile)
|
||||||
|
{
|
||||||
|
candidate = uri.LocalPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate = candidate.Replace('/', '\\');
|
||||||
|
return string.IsNullOrWhiteSpace(candidate) ? null : candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsOfficeFile(string extension)
|
private static bool IsOfficeFile(string extension)
|
||||||
{
|
{
|
||||||
return OfficeExtensions.Contains(extension) ||
|
return OfficeExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) ||
|
||||||
ExcelExtensions.Contains(extension) ||
|
ExcelExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) ||
|
||||||
PowerPointExtensions.Contains(extension);
|
PowerPointExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetIconGlyph(string extension)
|
private static string GetIconGlyph(string extension)
|
||||||
@@ -150,4 +703,40 @@ public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService
|
|||||||
{
|
{
|
||||||
return ShortcutHelper.GetShortcutTarget(lnkPath);
|
return ShortcutHelper.GetShortcutTarget(lnkPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object? GetPropertyValue(object? instance, string propertyName)
|
||||||
|
{
|
||||||
|
return instance?.GetType().GetProperty(propertyName)?.GetValue(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetStringProperty(object? instance, string propertyName)
|
||||||
|
{
|
||||||
|
return GetPropertyValue(instance, propertyName) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetIntProperty(object instance, string propertyName)
|
||||||
|
{
|
||||||
|
var value = GetPropertyValue(instance, propertyName);
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
int intValue => intValue,
|
||||||
|
short shortValue => shortValue,
|
||||||
|
long longValue => (int)longValue,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrySetProperty(object? instance, string propertyName, object value)
|
||||||
|
{
|
||||||
|
var property = instance?.GetType().GetProperty(propertyName);
|
||||||
|
if (property?.CanWrite == true)
|
||||||
|
{
|
||||||
|
property.SetValue(instance, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InvokeParameterlessMethod(object instance, string methodName)
|
||||||
|
{
|
||||||
|
instance.GetType().GetMethod(methodName, Type.EmptyTypes)?.Invoke(instance, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
310
LanMountainDesktop/Services/RemovableStorageService.cs
Normal file
310
LanMountainDesktop/Services/RemovableStorageService.cs
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
public sealed record RemovableStorageDrive(
|
||||||
|
string RootPath,
|
||||||
|
string DriveLetter,
|
||||||
|
string? VolumeLabel);
|
||||||
|
|
||||||
|
public interface IRemovableStorageService
|
||||||
|
{
|
||||||
|
IReadOnlyList<RemovableStorageDrive> GetConnectedDrives();
|
||||||
|
|
||||||
|
bool OpenDrive(string rootPath);
|
||||||
|
|
||||||
|
bool EjectDrive(string rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RemovableStorageService : IRemovableStorageService
|
||||||
|
{
|
||||||
|
public IReadOnlyList<RemovableStorageDrive> GetConnectedDrives()
|
||||||
|
{
|
||||||
|
var drives = new List<RemovableStorageDrive>();
|
||||||
|
|
||||||
|
foreach (var drive in DriveInfo.GetDrives())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (drive.DriveType != DriveType.Removable || !drive.IsReady)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootPath = NormalizeRootPath(drive.Name);
|
||||||
|
if (string.IsNullOrWhiteSpace(rootPath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var driveLetter = rootPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
var volumeLabel = string.IsNullOrWhiteSpace(drive.VolumeLabel)
|
||||||
|
? null
|
||||||
|
: drive.VolumeLabel.Trim();
|
||||||
|
|
||||||
|
drives.Add(new RemovableStorageDrive(rootPath, driveLetter, volumeLabel));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("RemovableStorage", $"Failed to inspect drive '{drive.Name}'.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return drives
|
||||||
|
.OrderBy(drive => drive.DriveLetter, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OpenDrive(string rootPath)
|
||||||
|
{
|
||||||
|
var normalizedRootPath = NormalizeRootPath(rootPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedRootPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = normalizedRootPath,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("RemovableStorage", $"Failed to open drive '{normalizedRootPath}'.", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool EjectDrive(string rootPath)
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedRootPath = NormalizeRootPath(rootPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedRootPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
object? shellApplication = null;
|
||||||
|
object? computerFolder = null;
|
||||||
|
object? driveItem = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var shellType = Type.GetTypeFromProgID("Shell.Application");
|
||||||
|
if (shellType is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
shellApplication = Activator.CreateInstance(shellType);
|
||||||
|
if (shellApplication is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
computerFolder = shellType.InvokeMember(
|
||||||
|
"NameSpace",
|
||||||
|
BindingFlags.InvokeMethod,
|
||||||
|
binder: null,
|
||||||
|
target: shellApplication,
|
||||||
|
args: [17]);
|
||||||
|
if (computerFolder is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var driveToken = normalizedRootPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
driveItem = computerFolder.GetType().InvokeMember(
|
||||||
|
"ParseName",
|
||||||
|
BindingFlags.InvokeMethod,
|
||||||
|
binder: null,
|
||||||
|
target: computerFolder,
|
||||||
|
args: [driveToken]);
|
||||||
|
if (driveItem is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryInvokeVerb(driveItem, "Eject"))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryInvokeLocalizedEjectVerb(driveItem);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("RemovableStorage", $"Failed to eject drive '{normalizedRootPath}'.", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseComObject(driveItem);
|
||||||
|
ReleaseComObject(computerFolder);
|
||||||
|
ReleaseComObject(shellApplication);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryInvokeLocalizedEjectVerb(object driveItem)
|
||||||
|
{
|
||||||
|
object? verbs = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
verbs = driveItem.GetType().InvokeMember(
|
||||||
|
"Verbs",
|
||||||
|
BindingFlags.InvokeMethod,
|
||||||
|
binder: null,
|
||||||
|
target: driveItem,
|
||||||
|
args: null);
|
||||||
|
if (verbs is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var verbsType = verbs.GetType();
|
||||||
|
var countObject = verbsType.InvokeMember(
|
||||||
|
"Count",
|
||||||
|
BindingFlags.GetProperty,
|
||||||
|
binder: null,
|
||||||
|
target: verbs,
|
||||||
|
args: null);
|
||||||
|
var count = countObject is null
|
||||||
|
? 0
|
||||||
|
: Convert.ToInt32(countObject, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
object? verb = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
verb = verbsType.InvokeMember(
|
||||||
|
"Item",
|
||||||
|
BindingFlags.InvokeMethod,
|
||||||
|
binder: null,
|
||||||
|
target: verbs,
|
||||||
|
args: [index]);
|
||||||
|
if (verb is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var verbNameObject = verb.GetType().InvokeMember(
|
||||||
|
"Name",
|
||||||
|
BindingFlags.GetProperty,
|
||||||
|
binder: null,
|
||||||
|
target: verb,
|
||||||
|
args: null);
|
||||||
|
var verbName = Convert.ToString(verbNameObject, CultureInfo.InvariantCulture);
|
||||||
|
if (!IsEjectVerbName(verbName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
verb.GetType().InvokeMember(
|
||||||
|
"DoIt",
|
||||||
|
BindingFlags.InvokeMethod,
|
||||||
|
binder: null,
|
||||||
|
target: verb,
|
||||||
|
args: null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseComObject(verb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseComObject(verbs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryInvokeVerb(object driveItem, string verbName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
driveItem.GetType().InvokeMember(
|
||||||
|
"InvokeVerb",
|
||||||
|
BindingFlags.InvokeMethod,
|
||||||
|
binder: null,
|
||||||
|
target: driveItem,
|
||||||
|
args: [verbName]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEjectVerbName(string? verbName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(verbName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = string.Concat(
|
||||||
|
verbName
|
||||||
|
.Where(character => !char.IsWhiteSpace(character) && character != '&'))
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
return normalized.Contains("Eject", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
normalized.Contains("弹出", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains("安全删除", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains("卸载", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeRootPath(string? rootPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rootPath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = rootPath.Trim();
|
||||||
|
if (trimmed.Length == 1 && char.IsLetter(trimmed[0]))
|
||||||
|
{
|
||||||
|
return string.Create(CultureInfo.InvariantCulture, $"{trimmed}:{Path.DirectorySeparatorChar}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.Length == 2 && char.IsLetter(trimmed[0]) && trimmed[1] == ':')
|
||||||
|
{
|
||||||
|
return trimmed + Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = trimmed.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||||
|
var resolvedRoot = Path.GetPathRoot(normalized);
|
||||||
|
return string.IsNullOrWhiteSpace(resolvedRoot)
|
||||||
|
? normalized
|
||||||
|
: resolvedRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReleaseComObject(object? value)
|
||||||
|
{
|
||||||
|
if (value is not null && Marshal.IsComObject(value))
|
||||||
|
{
|
||||||
|
Marshal.FinalReleaseComObject(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ internal sealed class SettingsCatalogService : ISettingsCatalog
|
|||||||
[
|
[
|
||||||
new SettingsSectionDefinition("general", SettingsCategories.General, SettingsScope.App, "settings.general.title", iconKey: "Settings", sortOrder: 0),
|
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("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("components", SettingsCategories.Components, SettingsScope.ComponentInstance, "settings.components.title", iconKey: "Apps", sortOrder: 20),
|
||||||
new SettingsSectionDefinition("plugins", SettingsCategories.Plugins, SettingsScope.Plugin, "settings.plugins.title", iconKey: "PuzzlePiece", sortOrder: 30),
|
new SettingsSectionDefinition("plugins", SettingsCategories.Plugins, SettingsScope.Plugin, "settings.plugins.title", iconKey: "PuzzlePiece", sortOrder: 30),
|
||||||
new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 40)
|
new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 40)
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
using LanMountainDesktop.Settings.Core;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services.Settings;
|
namespace LanMountainDesktop.Services.Settings;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ public sealed record ThemeAppearanceSettingsState(
|
|||||||
bool IsNightMode,
|
bool IsNightMode,
|
||||||
string? ThemeColor,
|
string? ThemeColor,
|
||||||
bool UseSystemChrome,
|
bool UseSystemChrome,
|
||||||
|
double GlobalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale,
|
||||||
string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral,
|
string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral,
|
||||||
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone,
|
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone,
|
||||||
string? SelectedWallpaperSeed = null);
|
string? SelectedWallpaperSeed = null);
|
||||||
@@ -29,6 +31,7 @@ public sealed record StatusBarSettingsState(
|
|||||||
bool EnableDynamicTaskbarActions,
|
bool EnableDynamicTaskbarActions,
|
||||||
string TaskbarLayoutMode,
|
string TaskbarLayoutMode,
|
||||||
string ClockDisplayFormat,
|
string ClockDisplayFormat,
|
||||||
|
bool ClockTransparentBackground,
|
||||||
string SpacingMode,
|
string SpacingMode,
|
||||||
int CustomSpacingPercent);
|
int CustomSpacingPercent);
|
||||||
public sealed record WeatherSettingsState(
|
public sealed record WeatherSettingsState(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Avalonia.Media.Imaging;
|
|||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
using LanMountainDesktop.Settings.Core;
|
||||||
using LanMountainDesktop.Services.PluginMarket;
|
using LanMountainDesktop.Services.PluginMarket;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services.Settings;
|
namespace LanMountainDesktop.Services.Settings;
|
||||||
@@ -242,6 +243,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
|
|||||||
snapshot.IsNightMode ?? false,
|
snapshot.IsNightMode ?? false,
|
||||||
snapshot.ThemeColor,
|
snapshot.ThemeColor,
|
||||||
snapshot.UseSystemChrome,
|
snapshot.UseSystemChrome,
|
||||||
|
GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale),
|
||||||
ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor),
|
ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor),
|
||||||
ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode),
|
ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode),
|
||||||
snapshot.SelectedWallpaperSeed);
|
snapshot.SelectedWallpaperSeed);
|
||||||
@@ -252,6 +254,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
|
|||||||
var snapshot = _settingsService.Load();
|
var snapshot = _settingsService.Load();
|
||||||
var changedKeys = new List<string>();
|
var changedKeys = new List<string>();
|
||||||
var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor;
|
var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor;
|
||||||
|
var normalizedCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(state.GlobalCornerRadiusScale);
|
||||||
var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor);
|
var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor);
|
||||||
var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode);
|
var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode);
|
||||||
var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed)
|
var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed)
|
||||||
@@ -276,6 +279,12 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
|
|||||||
changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome));
|
changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Math.Abs(GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale) - normalizedCornerRadiusScale) > 0.0001d)
|
||||||
|
{
|
||||||
|
snapshot.GlobalCornerRadiusScale = normalizedCornerRadiusScale;
|
||||||
|
changedKeys.Add(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale));
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
snapshot.ThemeColorMode = normalizedThemeColorMode;
|
snapshot.ThemeColorMode = normalizedThemeColorMode;
|
||||||
@@ -361,6 +370,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
|
|||||||
snapshot.EnableDynamicTaskbarActions,
|
snapshot.EnableDynamicTaskbarActions,
|
||||||
snapshot.TaskbarLayoutMode,
|
snapshot.TaskbarLayoutMode,
|
||||||
snapshot.ClockDisplayFormat,
|
snapshot.ClockDisplayFormat,
|
||||||
|
snapshot.StatusBarClockTransparentBackground,
|
||||||
snapshot.StatusBarSpacingMode,
|
snapshot.StatusBarSpacingMode,
|
||||||
snapshot.StatusBarCustomSpacingPercent);
|
snapshot.StatusBarCustomSpacingPercent);
|
||||||
}
|
}
|
||||||
@@ -373,6 +383,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
|
|||||||
snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions;
|
snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions;
|
||||||
snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode;
|
snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode;
|
||||||
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
|
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
|
||||||
|
snapshot.StatusBarClockTransparentBackground = state.ClockTransparentBackground;
|
||||||
snapshot.StatusBarSpacingMode = state.SpacingMode;
|
snapshot.StatusBarSpacingMode = state.SpacingMode;
|
||||||
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
|
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
|
||||||
_settingsService.SaveSnapshot(
|
_settingsService.SaveSnapshot(
|
||||||
@@ -385,6 +396,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
|
|||||||
nameof(AppSettingsSnapshot.EnableDynamicTaskbarActions),
|
nameof(AppSettingsSnapshot.EnableDynamicTaskbarActions),
|
||||||
nameof(AppSettingsSnapshot.TaskbarLayoutMode),
|
nameof(AppSettingsSnapshot.TaskbarLayoutMode),
|
||||||
nameof(AppSettingsSnapshot.ClockDisplayFormat),
|
nameof(AppSettingsSnapshot.ClockDisplayFormat),
|
||||||
|
nameof(AppSettingsSnapshot.StatusBarClockTransparentBackground),
|
||||||
nameof(AppSettingsSnapshot.StatusBarSpacingMode),
|
nameof(AppSettingsSnapshot.StatusBarSpacingMode),
|
||||||
nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent)
|
nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent)
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
|||||||
if (languageChanged)
|
if (languageChanged)
|
||||||
{
|
{
|
||||||
var regionState = _settingsFacade.Region.Get();
|
var regionState = _settingsFacade.Region.Get();
|
||||||
|
// 清除本地化缓存,强制重新加载语言文件
|
||||||
|
_localizationService.ClearCache();
|
||||||
_viewModel.RefreshLanguage(regionState.LanguageCode);
|
_viewModel.RefreshLanguage(regionState.LanguageCode);
|
||||||
_pageRegistry.Rebuild();
|
_pageRegistry.Rebuild();
|
||||||
_window.ReloadPages(_viewModel.CurrentPageId);
|
_window.ReloadPages(_viewModel.CurrentPageId);
|
||||||
|
|||||||
338
LanMountainDesktop/Services/WhiteboardNotePersistenceService.cs
Normal file
338
LanMountainDesktop/Services/WhiteboardNotePersistenceService.cs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenceService
|
||||||
|
{
|
||||||
|
private const int DefaultCleanupBatchSize = 256;
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly object _schemaSyncRoot = new();
|
||||||
|
private readonly AppDatabaseService _databaseService;
|
||||||
|
private bool _schemaInitialized;
|
||||||
|
|
||||||
|
public WhiteboardNotePersistenceService(AppDatabaseService? databaseService = null)
|
||||||
|
{
|
||||||
|
_databaseService = databaseService ?? AppDatabaseServiceFactory.CreateDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WhiteboardNoteSnapshot LoadNote(string componentId, string? placementId, int retentionDays)
|
||||||
|
{
|
||||||
|
if (!TryNormalizeKeys(componentId, placementId, out var normalizedComponentId, out var normalizedPlacementId))
|
||||||
|
{
|
||||||
|
return new WhiteboardNoteSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = OpenConnection();
|
||||||
|
DeleteExpiredInternal(
|
||||||
|
connection,
|
||||||
|
normalizedComponentId,
|
||||||
|
normalizedPlacementId,
|
||||||
|
WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays),
|
||||||
|
DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = """
|
||||||
|
SELECT note_json, saved_at_utc_ms
|
||||||
|
FROM whiteboard_notes
|
||||||
|
WHERE component_id = $componentId
|
||||||
|
AND placement_id = $placementId
|
||||||
|
LIMIT 1;
|
||||||
|
""";
|
||||||
|
command.Parameters.AddWithValue("$componentId", normalizedComponentId);
|
||||||
|
command.Parameters.AddWithValue("$placementId", normalizedPlacementId);
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
if (!reader.Read() || reader.IsDBNull(0))
|
||||||
|
{
|
||||||
|
return new WhiteboardNoteSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = reader.GetString(0);
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return new WhiteboardNoteSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = JsonSerializer.Deserialize<WhiteboardNoteSnapshot>(json, JsonOptions) ?? new WhiteboardNoteSnapshot();
|
||||||
|
if (!reader.IsDBNull(1))
|
||||||
|
{
|
||||||
|
snapshot.SavedUtc = DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsExpired(snapshot, retentionDays))
|
||||||
|
{
|
||||||
|
DeleteNote(normalizedComponentId, normalizedPlacementId);
|
||||||
|
return new WhiteboardNoteSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.Clone();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new WhiteboardNoteSnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveNote(string componentId, string? placementId, WhiteboardNoteSnapshot snapshot, int retentionDays)
|
||||||
|
{
|
||||||
|
if (!TryNormalizeKeys(componentId, placementId, out var normalizedComponentId, out var normalizedPlacementId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var nowUtc = DateTimeOffset.UtcNow;
|
||||||
|
var persistedSnapshot = snapshot?.Clone() ?? new WhiteboardNoteSnapshot();
|
||||||
|
persistedSnapshot.SavedUtc = nowUtc;
|
||||||
|
var expiresUtc = GetExpirationUtc(persistedSnapshot, retentionDays) ?? nowUtc.AddDays(WhiteboardNoteRetentionPolicy.DefaultDays);
|
||||||
|
var json = JsonSerializer.Serialize(persistedSnapshot, JsonOptions);
|
||||||
|
|
||||||
|
using var connection = OpenConnection();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = """
|
||||||
|
INSERT INTO whiteboard_notes(
|
||||||
|
component_id,
|
||||||
|
placement_id,
|
||||||
|
note_json,
|
||||||
|
saved_at_utc_ms,
|
||||||
|
expires_at_utc_ms,
|
||||||
|
updated_at_utc_ms)
|
||||||
|
VALUES(
|
||||||
|
$componentId,
|
||||||
|
$placementId,
|
||||||
|
$noteJson,
|
||||||
|
$savedAtUtcMs,
|
||||||
|
$expiresAtUtcMs,
|
||||||
|
$updatedAtUtcMs)
|
||||||
|
ON CONFLICT(component_id, placement_id) DO UPDATE SET
|
||||||
|
note_json = excluded.note_json,
|
||||||
|
saved_at_utc_ms = excluded.saved_at_utc_ms,
|
||||||
|
expires_at_utc_ms = excluded.expires_at_utc_ms,
|
||||||
|
updated_at_utc_ms = excluded.updated_at_utc_ms;
|
||||||
|
""";
|
||||||
|
command.Parameters.AddWithValue("$componentId", normalizedComponentId);
|
||||||
|
command.Parameters.AddWithValue("$placementId", normalizedPlacementId);
|
||||||
|
command.Parameters.AddWithValue("$noteJson", json);
|
||||||
|
command.Parameters.AddWithValue("$savedAtUtcMs", persistedSnapshot.SavedUtc.ToUnixTimeMilliseconds());
|
||||||
|
command.Parameters.AddWithValue("$expiresAtUtcMs", expiresUtc.ToUnixTimeMilliseconds());
|
||||||
|
command.Parameters.AddWithValue("$updatedAtUtcMs", nowUtc.ToUnixTimeMilliseconds());
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Keep whiteboard usable even when persistence is unavailable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DeleteNote(string componentId, string? placementId)
|
||||||
|
{
|
||||||
|
if (!TryNormalizeKeys(componentId, placementId, out var normalizedComponentId, out var normalizedPlacementId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = OpenConnection();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = """
|
||||||
|
DELETE FROM whiteboard_notes
|
||||||
|
WHERE component_id = $componentId
|
||||||
|
AND placement_id = $placementId;
|
||||||
|
""";
|
||||||
|
command.Parameters.AddWithValue("$componentId", normalizedComponentId);
|
||||||
|
command.Parameters.AddWithValue("$placementId", normalizedPlacementId);
|
||||||
|
return command.ExecuteNonQuery() > 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryDeleteExpiredNote(string componentId, string? placementId, int retentionDays)
|
||||||
|
{
|
||||||
|
if (!TryNormalizeKeys(componentId, placementId, out var normalizedComponentId, out var normalizedPlacementId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = OpenConnection();
|
||||||
|
return DeleteExpiredInternal(
|
||||||
|
connection,
|
||||||
|
normalizedComponentId,
|
||||||
|
normalizedPlacementId,
|
||||||
|
WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays),
|
||||||
|
DateTimeOffset.UtcNow);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int DeleteExpiredNotesBatch(int batchSize = DefaultCleanupBatchSize, DateTimeOffset? now = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = OpenConnection();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = """
|
||||||
|
DELETE FROM whiteboard_notes
|
||||||
|
WHERE rowid IN (
|
||||||
|
SELECT rowid
|
||||||
|
FROM whiteboard_notes
|
||||||
|
WHERE expires_at_utc_ms <= $nowUtcMs
|
||||||
|
ORDER BY expires_at_utc_ms ASC
|
||||||
|
LIMIT $batchSize
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
command.Parameters.AddWithValue("$nowUtcMs", (now ?? DateTimeOffset.UtcNow).ToUnixTimeMilliseconds());
|
||||||
|
command.Parameters.AddWithValue("$batchSize", NormalizeBatchSize(batchSize));
|
||||||
|
return command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsExpired(WhiteboardNoteSnapshot snapshot, int retentionDays, DateTimeOffset? now = null)
|
||||||
|
{
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expirationUtc = GetExpirationUtc(snapshot, retentionDays);
|
||||||
|
if (!expirationUtc.HasValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expirationUtc.Value <= (now ?? DateTimeOffset.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTimeOffset? GetExpirationUtc(WhiteboardNoteSnapshot snapshot, int retentionDays)
|
||||||
|
{
|
||||||
|
if (snapshot is null || snapshot.SavedUtc == default)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.SavedUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SqliteConnection OpenConnection()
|
||||||
|
{
|
||||||
|
var connection = _databaseService.OpenConnection();
|
||||||
|
EnsureSchema(connection);
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureSchema(SqliteConnection connection)
|
||||||
|
{
|
||||||
|
if (_schemaInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_schemaSyncRoot)
|
||||||
|
{
|
||||||
|
if (_schemaInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = """
|
||||||
|
CREATE TABLE IF NOT EXISTS whiteboard_notes (
|
||||||
|
component_id TEXT NOT NULL,
|
||||||
|
placement_id TEXT NOT NULL,
|
||||||
|
note_json TEXT NOT NULL,
|
||||||
|
saved_at_utc_ms INTEGER NOT NULL,
|
||||||
|
expires_at_utc_ms INTEGER NOT NULL,
|
||||||
|
updated_at_utc_ms INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (component_id, placement_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whiteboard_notes_expires_at
|
||||||
|
ON whiteboard_notes(expires_at_utc_ms);
|
||||||
|
""";
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
_schemaInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DeleteExpiredInternal(
|
||||||
|
SqliteConnection connection,
|
||||||
|
string componentId,
|
||||||
|
string placementId,
|
||||||
|
int retentionDays,
|
||||||
|
DateTimeOffset nowUtc)
|
||||||
|
{
|
||||||
|
using var selectCommand = connection.CreateCommand();
|
||||||
|
selectCommand.CommandText = """
|
||||||
|
SELECT saved_at_utc_ms
|
||||||
|
FROM whiteboard_notes
|
||||||
|
WHERE component_id = $componentId
|
||||||
|
AND placement_id = $placementId
|
||||||
|
LIMIT 1;
|
||||||
|
""";
|
||||||
|
selectCommand.Parameters.AddWithValue("$componentId", componentId);
|
||||||
|
selectCommand.Parameters.AddWithValue("$placementId", placementId);
|
||||||
|
|
||||||
|
var scalar = selectCommand.ExecuteScalar();
|
||||||
|
if (scalar is not long savedAtUtcMs)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedUtc = DateTimeOffset.FromUnixTimeMilliseconds(savedAtUtcMs);
|
||||||
|
var expiresUtc = savedUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
|
||||||
|
if (expiresUtc > nowUtc)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var deleteCommand = connection.CreateCommand();
|
||||||
|
deleteCommand.CommandText = """
|
||||||
|
DELETE FROM whiteboard_notes
|
||||||
|
WHERE component_id = $componentId
|
||||||
|
AND placement_id = $placementId;
|
||||||
|
""";
|
||||||
|
deleteCommand.Parameters.AddWithValue("$componentId", componentId);
|
||||||
|
deleteCommand.Parameters.AddWithValue("$placementId", placementId);
|
||||||
|
return deleteCommand.ExecuteNonQuery() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryNormalizeKeys(
|
||||||
|
string componentId,
|
||||||
|
string? placementId,
|
||||||
|
out string normalizedComponentId,
|
||||||
|
out string normalizedPlacementId)
|
||||||
|
{
|
||||||
|
normalizedComponentId = componentId?.Trim() ?? string.Empty;
|
||||||
|
normalizedPlacementId = placementId?.Trim() ?? string.Empty;
|
||||||
|
return !string.IsNullOrWhiteSpace(normalizedComponentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NormalizeBatchSize(int batchSize)
|
||||||
|
{
|
||||||
|
return batchSize <= 0
|
||||||
|
? DefaultCleanupBatchSize
|
||||||
|
: Math.Clamp(batchSize, 1, 4096);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<Styles xmlns="https://github.com/avaloniaui"
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:assists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles">
|
||||||
<Style Selector="Window.component-editor-window">
|
<Style Selector="Window.component-editor-window">
|
||||||
<Setter Property="Background" Value="{DynamicResource EditorWindowBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource EditorWindowBackgroundBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource EditorSelectFieldBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource EditorSelectFieldBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource EditorSelectOutlineBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource EditorSelectOutlineBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="18" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusMd}" />
|
||||||
<Setter Property="Padding" Value="16,14,12,14" />
|
<Setter Property="Padding" Value="16,14,12,14" />
|
||||||
<Setter Property="MinHeight" Value="56" />
|
<Setter Property="MinHeight" Value="56" />
|
||||||
<Setter Property="FontSize" Value="14" />
|
<Setter Property="FontSize" Value="14" />
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource EditorSelectFieldBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource EditorSelectFieldBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource EditorSelectOutlineBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource EditorSelectOutlineBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="18" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusMd}" />
|
||||||
<Setter Property="Padding" Value="16,14,12,14" />
|
<Setter Property="Padding" Value="16,14,12,14" />
|
||||||
<Setter Property="MinHeight" Value="56" />
|
<Setter Property="MinHeight" Value="56" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ComponentEditorPrimaryTextBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource ComponentEditorPrimaryTextBrush}" />
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="Padding" Value="16,12" />
|
<Setter Property="Padding" Value="16,12" />
|
||||||
<Setter Property="Margin" Value="6,4" />
|
<Setter Property="Margin" Value="6,4" />
|
||||||
<Setter Property="CornerRadius" Value="14" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||||
<Setter Property="MinHeight" Value="44" />
|
<Setter Property="MinHeight" Value="44" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
@@ -74,7 +75,21 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Window.component-editor-window RadioButton">
|
<Style Selector="Window.component-editor-window RadioButton">
|
||||||
|
<Setter Property="Theme" Value="{StaticResource MaterialRadioButton}" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource ComponentEditorPrimaryTextBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource ComponentEditorPrimaryTextBrush}" />
|
||||||
|
<Setter Property="Margin" Value="0,2" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="assists:SelectionControlAssist.Size" Value="20" />
|
||||||
|
<Setter Property="assists:SelectionControlAssist.Foreground" Value="{DynamicResource ComponentEditorSecondaryTextBrush}" />
|
||||||
|
<Setter Property="assists:SelectionControlAssist.InnerForeground" Value="{DynamicResource EditorPrimaryBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Window.component-editor-window RadioButton:pointerover">
|
||||||
|
<Setter Property="assists:SelectionControlAssist.Foreground" Value="{DynamicResource EditorSelectOutlineStrongBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Window.component-editor-window RadioButton:checked">
|
||||||
|
<Setter Property="assists:SelectionControlAssist.Foreground" Value="{DynamicResource EditorPrimaryBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Window.component-editor-window ToggleSwitch">
|
<Style Selector="Window.component-editor-window ToggleSwitch">
|
||||||
@@ -85,7 +100,7 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource EditorSurfaceContainerHighBrush}" />
|
<Setter Property="Background" Value="{DynamicResource EditorSurfaceContainerHighBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource EditorSelectOutlineBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource EditorSelectOutlineBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="20" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusLg}" />
|
||||||
<Setter Property="Padding" Value="4" />
|
<Setter Property="Padding" Value="4" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
@@ -93,7 +108,7 @@
|
|||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="Transparent" />
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="16" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||||
<Setter Property="Padding" Value="18,12" />
|
<Setter Property="Padding" Value="18,12" />
|
||||||
<Setter Property="MinHeight" Value="48" />
|
<Setter Property="MinHeight" Value="48" />
|
||||||
<Setter Property="FontSize" Value="14" />
|
<Setter Property="FontSize" Value="14" />
|
||||||
@@ -124,14 +139,14 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource ComponentEditorHeroBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource ComponentEditorHeroBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource ComponentEditorCardBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource ComponentEditorCardBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="28" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXl}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.component-editor-card">
|
<Style Selector="Border.component-editor-card">
|
||||||
<Setter Property="Background" Value="{DynamicResource ComponentEditorCardBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource ComponentEditorCardBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource ComponentEditorCardBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource ComponentEditorCardBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="24" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusLg}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBlock.component-editor-headline">
|
<Style Selector="TextBlock.component-editor-headline">
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
<Styles.Resources>
|
<Styles.Resources>
|
||||||
<!-- Unified corner radius tokens used across settings and widget panels -->
|
<!-- Unified corner radius tokens used across settings and widget panels -->
|
||||||
|
<CornerRadius x:Key="DesignCornerRadiusMicro">6</CornerRadius>
|
||||||
<CornerRadius x:Key="DesignCornerRadiusXl">32</CornerRadius>
|
<CornerRadius x:Key="DesignCornerRadiusXl">32</CornerRadius>
|
||||||
<CornerRadius x:Key="DesignCornerRadiusLg">28</CornerRadius>
|
<CornerRadius x:Key="DesignCornerRadiusLg">28</CornerRadius>
|
||||||
<CornerRadius x:Key="DesignCornerRadiusMd">20</CornerRadius>
|
<CornerRadius x:Key="DesignCornerRadiusMd">20</CornerRadius>
|
||||||
<CornerRadius x:Key="DesignCornerRadiusSm">14</CornerRadius>
|
<CornerRadius x:Key="DesignCornerRadiusSm">14</CornerRadius>
|
||||||
<CornerRadius x:Key="DesignCornerRadiusXs">12</CornerRadius>
|
<CornerRadius x:Key="DesignCornerRadiusXs">12</CornerRadius>
|
||||||
|
<CornerRadius x:Key="DesignCornerRadiusIsland">36</CornerRadius>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
|
|
||||||
<Style Selector="TextBlock">
|
<Style Selector="TextBlock">
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||||
<Setter Property="CornerRadius" Value="20" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusMd}" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
<Setter Property="FontSize" Value="14" />
|
<Setter Property="FontSize" Value="14" />
|
||||||
<Setter Property="Padding" Value="16,10" />
|
<Setter Property="Padding" Value="16,10" />
|
||||||
@@ -155,7 +157,7 @@
|
|||||||
|
|
||||||
<Style Selector="Button.swatch-button">
|
<Style Selector="Button.swatch-button">
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="16" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||||
<Setter Property="Opacity" Value="0.88" />
|
<Setter Property="Opacity" Value="0.88" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
@@ -165,29 +167,35 @@
|
|||||||
<Setter Property="RenderTransform" Value="scale(1.05)" />
|
<Setter Property="RenderTransform" Value="scale(1.05)" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.glass-panel">
|
<!--
|
||||||
|
半透明表面样式类
|
||||||
|
注意:这些样式使用纯色半透明画刷模拟玻璃效果,并非真正的 Mica/Acrylic 模糊材质。
|
||||||
|
真正的 Mica/Acrylic 效果仅通过 WindowTransparencyLevel 在独立窗口上应用。
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Style Selector="Border.surface-translucent-panel">
|
||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1.2" />
|
<Setter Property="BorderThickness" Value="1.2" />
|
||||||
<Setter Property="CornerRadius" Value="28" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusLg}" />
|
||||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassPanelOpacity}" />
|
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassPanelOpacity}" />
|
||||||
<Setter Property="BoxShadow" Value="0 4 12 #1A000000" />
|
<Setter Property="BoxShadow" Value="0 4 12 #1A000000" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.glass-strong">
|
<Style Selector="Border.surface-translucent-strong">
|
||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1.5" />
|
<Setter Property="BorderThickness" Value="1.5" />
|
||||||
<Setter Property="CornerRadius" Value="32" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXl}" />
|
||||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
||||||
<Setter Property="BoxShadow" Value="0 8 24 #26000000" />
|
<Setter Property="BoxShadow" Value="0 8 24 #26000000" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.glass-island">
|
<Style Selector="Border.surface-translucent-island">
|
||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveDockGlassBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveDockGlassBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveDockGlassBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveDockGlassBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1.5" />
|
<Setter Property="BorderThickness" Value="1.5" />
|
||||||
<Setter Property="CornerRadius" Value="36" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusIsland}" />
|
||||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
||||||
<Setter Property="BoxShadow" Value="0 12 32 #33000000" />
|
<Setter Property="BoxShadow" Value="0 12 32 #33000000" />
|
||||||
<Setter Property="Transitions">
|
<Setter Property="Transitions">
|
||||||
@@ -197,19 +205,26 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.mica-strong">
|
<Style Selector="Border.surface-solid-strong">
|
||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="36" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusIsland}" />
|
||||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
|
||||||
<Setter Property="BoxShadow" Value="0 8 22 #2A000000" />
|
<Setter Property="BoxShadow" Value="0 8 22 #2A000000" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.glass-overlay">
|
<Style Selector="Border.surface-translucent-overlay">
|
||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassOverlayBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassOverlayBackgroundBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
<Setter Property="CornerRadius" Value="0" />
|
||||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassOverlayOpacity}" />
|
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassOverlayOpacity}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- 向后兼容的旧样式类(已弃用) -->
|
||||||
|
<Style Selector="Border.glass-panel" />
|
||||||
|
<Style Selector="Border.glass-strong" />
|
||||||
|
<Style Selector="Border.glass-island" />
|
||||||
|
<Style Selector="Border.mica-strong" />
|
||||||
|
<Style Selector="Border.glass-overlay" />
|
||||||
|
|
||||||
</Styles>
|
</Styles>
|
||||||
|
|||||||
@@ -48,21 +48,21 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.settings-section-card">
|
<Style Selector="Border.settings-section-card">
|
||||||
<Setter Property="CornerRadius" Value="18" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusLg}" />
|
||||||
<Setter Property="Padding" Value="20" />
|
<Setter Property="Padding" Value="20" />
|
||||||
<Setter Property="Margin" Value="0,0,0,14" />
|
<Setter Property="Margin" Value="0,0,0,14" />
|
||||||
<Setter Property="BoxShadow" Value="0 2 8 #12000000" />
|
<Setter Property="BoxShadow" Value="0 2 8 #12000000" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.settings-option-card">
|
<Style Selector="Border.settings-option-card">
|
||||||
<Setter Property="CornerRadius" Value="14" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||||
<Setter Property="Padding" Value="16" />
|
<Setter Property="Padding" Value="16" />
|
||||||
<Setter Property="Margin" Value="0,0,0,12" />
|
<Setter Property="Margin" Value="0,0,0,12" />
|
||||||
<Setter Property="BoxShadow" Value="0 1 4 #0F000000" />
|
<Setter Property="BoxShadow" Value="0 1 4 #0F000000" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.settings-list-item">
|
<Style Selector="Border.settings-list-item">
|
||||||
<Setter Property="CornerRadius" Value="14" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||||
<Setter Property="Padding" Value="16" />
|
<Setter Property="Padding" Value="16" />
|
||||||
<Setter Property="Margin" Value="0,0,0,10" />
|
<Setter Property="Margin" Value="0,0,0,10" />
|
||||||
<Setter Property="BoxShadow" Value="0 1 4 #0F000000" />
|
<Setter Property="BoxShadow" Value="0 1 4 #0F000000" />
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="12" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||||
<Setter Property="VerticalAlignment" Value="Top" />
|
<Setter Property="VerticalAlignment" Value="Top" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="12" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||||
<Setter Property="Padding" Value="14,12" />
|
<Setter Property="Padding" Value="14,12" />
|
||||||
<Setter Property="Margin" Value="0,0,0,8" />
|
<Setter Property="Margin" Value="0,0,0,8" />
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveAccentBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveAccentBrush}" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveOnAccentBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource AdaptiveOnAccentBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="10" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||||
<Setter Property="Padding" Value="16,10" />
|
<Setter Property="Padding" Value="16,10" />
|
||||||
<Setter Property="MinHeight" Value="36" />
|
<Setter Property="MinHeight" Value="36" />
|
||||||
</Style>
|
</Style>
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
<Setter Property="Width" Value="36" />
|
<Setter Property="Width" Value="36" />
|
||||||
<Setter Property="Height" Value="36" />
|
<Setter Property="Height" Value="36" />
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="10" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||||
<Setter Property="MinHeight" Value="36" />
|
<Setter Property="MinHeight" Value="36" />
|
||||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||||
|
|||||||
@@ -564,11 +564,19 @@ public sealed partial class PluginMarketSettingsPageViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
RefreshInstalledSnapshot();
|
RefreshInstalledSnapshot();
|
||||||
RefreshItemStates();
|
RefreshItemStates();
|
||||||
|
|
||||||
|
// 设置更明显的状态消息
|
||||||
|
var pluginName = result.PluginName ?? item.Name;
|
||||||
StatusMessage = string.Format(
|
StatusMessage = string.Format(
|
||||||
CultureInfo.CurrentCulture,
|
CultureInfo.CurrentCulture,
|
||||||
L("market.status.install_success_format", "Plugin '{0}' has been staged. Restart the app to apply it."),
|
L("market.status.install_success_restart_format", "✓ Plugin '{0}' installed successfully! Please restart the application to activate it."),
|
||||||
result.PluginName ?? item.Name);
|
pluginName);
|
||||||
RestartRequested?.Invoke(RestartRequiredMessage);
|
|
||||||
|
// 触发重启提醒
|
||||||
|
RestartRequested?.Invoke(string.Format(
|
||||||
|
CultureInfo.CurrentCulture,
|
||||||
|
L("market.dialog.restart_message_format", "Plugin '{0}' has been installed successfully.\n\nTo use this plugin, you need to restart the application now.\n\nWould you like to restart?"),
|
||||||
|
pluginName));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using LanMountainDesktop.Models;
|
|||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
|
using LanMountainDesktop.Settings.Core;
|
||||||
|
|
||||||
namespace LanMountainDesktop.ViewModels;
|
namespace LanMountainDesktop.ViewModels;
|
||||||
|
|
||||||
@@ -268,12 +269,17 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
|
|||||||
|
|
||||||
partial void OnSelectedLanguageChanged(SelectionOption value)
|
partial void OnSelectedLanguageChanged(SelectionOption value)
|
||||||
{
|
{
|
||||||
RefreshPreview();
|
|
||||||
if (_isInitializing || value is null)
|
if (_isInitializing || value is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新语言代码并刷新UI文本
|
||||||
|
_languageCode = _localizationService.NormalizeLanguageCode(value.Value);
|
||||||
|
RefreshLocalizedText();
|
||||||
|
RefreshPreview();
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
_settingsFacade.Region.Save(new RegionSettingsState(
|
_settingsFacade.Region.Save(new RegionSettingsState(
|
||||||
value.Value,
|
value.Value,
|
||||||
NormalizeTimeZoneId(SelectedTimeZone?.Id)));
|
NormalizeTimeZoneId(SelectedTimeZone?.Id)));
|
||||||
@@ -476,6 +482,9 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _useSystemChrome;
|
private bool _useSystemChrome;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private SelectionOption _selectedThemeColorMode = new(ThemeAppearanceValues.ColorModeSeedMonet, "User theme color Monet");
|
private SelectionOption _selectedThemeColorMode = new(ThemeAppearanceValues.ColorModeSeedMonet, "User theme color Monet");
|
||||||
|
|
||||||
@@ -542,6 +551,12 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _systemMaterialLabel = string.Empty;
|
private string _systemMaterialLabel = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _globalCornerRadiusLabel = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _globalCornerRadiusDescription = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _themeHeader = string.Empty;
|
private string _themeHeader = string.Empty;
|
||||||
|
|
||||||
@@ -663,6 +678,32 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
|||||||
PersistCurrentState(restartRequired: false);
|
PersistCurrentState(restartRequired: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnGlobalCornerRadiusScaleChanged(double value)
|
||||||
|
{
|
||||||
|
if (_isInitializing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value);
|
||||||
|
if (Math.Abs(normalized - value) > 0.0001d)
|
||||||
|
{
|
||||||
|
_isInitializing = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GlobalCornerRadiusScale = normalized;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistCurrentState(restartRequired: false);
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnSelectedThemeColorModeChanged(SelectionOption value)
|
partial void OnSelectedThemeColorModeChanged(SelectionOption value)
|
||||||
{
|
{
|
||||||
if (_isInitializing || value is null)
|
if (_isInitializing || value is null)
|
||||||
@@ -727,6 +768,8 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
|||||||
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
|
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
|
||||||
ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
|
ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
|
||||||
SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
|
SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
|
||||||
|
GlobalCornerRadiusLabel = L("settings.appearance.corner_radius.label", "Global corner radius");
|
||||||
|
GlobalCornerRadiusDescription = L("settings.appearance.corner_radius.description", "Adjust the shared radius scale used by cards, panels, and component containers.");
|
||||||
ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
|
ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
|
||||||
ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
|
ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
|
||||||
ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
|
ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
|
||||||
@@ -771,6 +814,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
|||||||
IsNightMode = theme.IsNightMode;
|
IsNightMode = theme.IsNightMode;
|
||||||
ThemeColor = theme.ThemeColor ?? string.Empty;
|
ThemeColor = theme.ThemeColor ?? string.Empty;
|
||||||
UseSystemChrome = theme.UseSystemChrome;
|
UseSystemChrome = theme.UseSystemChrome;
|
||||||
|
GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale);
|
||||||
_selectedWallpaperSeed = theme.SelectedWallpaperSeed;
|
_selectedWallpaperSeed = theme.SelectedWallpaperSeed;
|
||||||
SyncCustomSeedPickerWithSavedThemeColor();
|
SyncCustomSeedPickerWithSavedThemeColor();
|
||||||
|
|
||||||
@@ -820,6 +864,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
|
|||||||
IsNightMode,
|
IsNightMode,
|
||||||
themeColor,
|
themeColor,
|
||||||
UseSystemChrome,
|
UseSystemChrome,
|
||||||
|
GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale),
|
||||||
themeColorMode,
|
themeColorMode,
|
||||||
ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
|
ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
|
||||||
_selectedWallpaperSeed);
|
_selectedWallpaperSeed);
|
||||||
@@ -951,7 +996,7 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
|
|||||||
private string _pageDescription = string.Empty;
|
private string _pageDescription = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _gridHeader = string.Empty;
|
private string _componentsHeader = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _shortSideCellsLabel = string.Empty;
|
private string _shortSideCellsLabel = string.Empty;
|
||||||
@@ -962,6 +1007,18 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _spacingPresetLabel = string.Empty;
|
private string _spacingPresetLabel = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _componentRadiusHeader = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _globalCornerRadiusLabel = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _globalCornerRadiusDescription = string.Empty;
|
||||||
|
|
||||||
public void Load()
|
public void Load()
|
||||||
{
|
{
|
||||||
var state = _settingsFacade.Grid.Get();
|
var state = _settingsFacade.Grid.Get();
|
||||||
@@ -971,6 +1028,9 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
|
|||||||
SelectedSpacingPreset = SpacingPresets.FirstOrDefault(option =>
|
SelectedSpacingPreset = SpacingPresets.FirstOrDefault(option =>
|
||||||
string.Equals(option.Value, spacingPreset, StringComparison.OrdinalIgnoreCase))
|
string.Equals(option.Value, spacingPreset, StringComparison.OrdinalIgnoreCase))
|
||||||
?? SpacingPresets[1];
|
?? SpacingPresets[1];
|
||||||
|
|
||||||
|
var theme = _settingsFacade.Theme.Get();
|
||||||
|
GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnShortSideCellsChanged(int value)
|
partial void OnShortSideCellsChanged(int value)
|
||||||
@@ -1003,6 +1063,32 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
|
|||||||
SaveGrid();
|
SaveGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnGlobalCornerRadiusScaleChanged(double value)
|
||||||
|
{
|
||||||
|
if (_isInitializing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value);
|
||||||
|
if (Math.Abs(normalized - value) > 0.0001d)
|
||||||
|
{
|
||||||
|
_isInitializing = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GlobalCornerRadiusScale = normalized;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveComponentCornerRadius();
|
||||||
|
}
|
||||||
|
|
||||||
private void SaveGrid()
|
private void SaveGrid()
|
||||||
{
|
{
|
||||||
_settingsFacade.Grid.Save(new GridSettingsState(
|
_settingsFacade.Grid.Save(new GridSettingsState(
|
||||||
@@ -1011,23 +1097,39 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
|
|||||||
Math.Clamp(EdgeInsetPercent, 0, 30)));
|
Math.Clamp(EdgeInsetPercent, 0, 30)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SaveComponentCornerRadius()
|
||||||
|
{
|
||||||
|
var theme = _settingsFacade.Theme.Get();
|
||||||
|
_settingsFacade.Theme.Save(new ThemeAppearanceSettingsState(
|
||||||
|
theme.IsNightMode,
|
||||||
|
theme.ThemeColor,
|
||||||
|
theme.UseSystemChrome,
|
||||||
|
GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale),
|
||||||
|
theme.ThemeColorMode,
|
||||||
|
theme.SystemMaterialMode,
|
||||||
|
theme.SelectedWallpaperSeed));
|
||||||
|
}
|
||||||
|
|
||||||
private IReadOnlyList<SelectionOption> CreateSpacingPresets()
|
private IReadOnlyList<SelectionOption> CreateSpacingPresets()
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
[
|
[
|
||||||
new SelectionOption("Compact", L("settings.grid.spacing_compact", "Compact")),
|
new SelectionOption("Compact", L("settings.components.spacing_compact", "Compact")),
|
||||||
new SelectionOption("Relaxed", L("settings.grid.spacing_relaxed", "Relaxed"))
|
new SelectionOption("Relaxed", L("settings.components.spacing_relaxed", "Relaxed"))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshLocalizedText()
|
private void RefreshLocalizedText()
|
||||||
{
|
{
|
||||||
PageTitle = L("settings.components.title", "Components");
|
PageTitle = L("settings.components.title", "Components");
|
||||||
PageDescription = L("settings.components.description", "Desktop grid and widget placement density.");
|
PageDescription = L("settings.components.description", "Adjust component layout and corner design.");
|
||||||
GridHeader = L("settings.components.grid_header", "Grid Layout");
|
ComponentsHeader = L("settings.components.header", "Grid Settings");
|
||||||
ShortSideCellsLabel = L("settings.grid.short_side_label", "Short Side Cells");
|
ShortSideCellsLabel = L("settings.components.short_side_label", "Short Side Cells");
|
||||||
EdgeInsetPercentLabel = L("settings.grid.edge_inset_label", "Screen Inset");
|
EdgeInsetPercentLabel = L("settings.components.edge_inset_label", "Screen Inset");
|
||||||
SpacingPresetLabel = L("settings.grid.spacing_label", "Grid Spacing");
|
SpacingPresetLabel = L("settings.components.spacing_label", "Component Spacing");
|
||||||
|
ComponentRadiusHeader = L("settings.components.corner_radius.header", "Corner Design");
|
||||||
|
GlobalCornerRadiusLabel = L("settings.components.corner_radius.label", "Component Corner Radius");
|
||||||
|
GlobalCornerRadiusDescription = L("settings.components.corner_radius.description", "Adjust the shared corner radius used by component containers, and expand the internal safe area with it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string L(string key, string fallback)
|
private string L(string key, string fallback)
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private SelectionOption _selectedClockFormat = new("HourMinuteSecond", "Hour:Minute:Second");
|
private SelectionOption _selectedClockFormat = new("HourMinuteSecond", "Hour:Minute:Second");
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _clockTransparentBackground;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed");
|
private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed");
|
||||||
|
|
||||||
@@ -66,6 +69,12 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _clockFormatLabel = string.Empty;
|
private string _clockFormatLabel = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _clockTransparentBackgroundLabel = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _clockTransparentBackgroundDescription = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _spacingHeader = string.Empty;
|
private string _spacingHeader = string.Empty;
|
||||||
|
|
||||||
@@ -88,6 +97,7 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
SelectedClockFormat = ClockFormats.FirstOrDefault(option =>
|
SelectedClockFormat = ClockFormats.FirstOrDefault(option =>
|
||||||
string.Equals(option.Value, clockFormat, StringComparison.OrdinalIgnoreCase))
|
string.Equals(option.Value, clockFormat, StringComparison.OrdinalIgnoreCase))
|
||||||
?? ClockFormats[1];
|
?? ClockFormats[1];
|
||||||
|
ClockTransparentBackground = state.ClockTransparentBackground;
|
||||||
|
|
||||||
var spacingMode = NormalizeSpacingMode(state.SpacingMode);
|
var spacingMode = NormalizeSpacingMode(state.SpacingMode);
|
||||||
SelectedSpacingMode = SpacingModes.FirstOrDefault(option =>
|
SelectedSpacingMode = SpacingModes.FirstOrDefault(option =>
|
||||||
@@ -117,6 +127,16 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnClockTransparentBackgroundChanged(bool value)
|
||||||
|
{
|
||||||
|
if (_isInitializing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnSelectedSpacingModeChanged(SelectionOption value)
|
partial void OnSelectedSpacingModeChanged(SelectionOption value)
|
||||||
{
|
{
|
||||||
IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase);
|
IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -163,6 +183,7 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
state.EnableDynamicTaskbarActions,
|
state.EnableDynamicTaskbarActions,
|
||||||
state.TaskbarLayoutMode,
|
state.TaskbarLayoutMode,
|
||||||
SelectedClockFormat.Value,
|
SelectedClockFormat.Value,
|
||||||
|
ClockTransparentBackground,
|
||||||
NormalizeSpacingMode(SelectedSpacingMode.Value),
|
NormalizeSpacingMode(SelectedSpacingMode.Value),
|
||||||
Math.Clamp(CustomSpacingPercent, 0, 30)));
|
Math.Clamp(CustomSpacingPercent, 0, 30)));
|
||||||
}
|
}
|
||||||
@@ -194,6 +215,8 @@ public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
|
|||||||
ClockHeader = L("settings.status_bar.clock_header", "Clock Component");
|
ClockHeader = L("settings.status_bar.clock_header", "Clock Component");
|
||||||
ClockDescription = L("settings.status_bar.clock_description", "Display a clock on the top status bar.");
|
ClockDescription = L("settings.status_bar.clock_description", "Display a clock on the top status bar.");
|
||||||
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format");
|
ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format");
|
||||||
|
ClockTransparentBackgroundLabel = L("settings.status_bar.clock_transparent_background_label", "Transparent background");
|
||||||
|
ClockTransparentBackgroundDescription = L("settings.status_bar.clock_transparent_background_desc", "Remove the capsule background and keep only the clock text.");
|
||||||
SpacingHeader = L("settings.status_bar.spacing_header", "Component Spacing");
|
SpacingHeader = L("settings.status_bar.spacing_header", "Component Spacing");
|
||||||
SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
|
SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
|
||||||
CustomSpacingLabel = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
|
CustomSpacingLabel = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
<!-- MD3 Button Styles -->
|
<!-- MD3 Button Styles -->
|
||||||
<Style Selector="Button.component-editor-footer-button">
|
<Style Selector="Button.component-editor-footer-button">
|
||||||
<Setter Property="CornerRadius" Value="20" />
|
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusMd}" />
|
||||||
<Setter Property="Background" Value="{DynamicResource EditorPrimaryBrush}" />
|
<Setter Property="Background" Value="{DynamicResource EditorPrimaryBrush}" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource EditorOnPrimaryBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource EditorOnPrimaryBrush}" />
|
||||||
<Setter Property="Height" Value="40" />
|
<Setter Property="Height" Value="40" />
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
Height="64"
|
Height="64"
|
||||||
Background="{DynamicResource EditorPrimaryBrush}"
|
Background="{DynamicResource EditorPrimaryBrush}"
|
||||||
Foreground="{DynamicResource EditorOnPrimaryBrush}"
|
Foreground="{DynamicResource EditorOnPrimaryBrush}"
|
||||||
CornerRadius="18"
|
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||||
Classes="accent"
|
Classes="accent"
|
||||||
Click="OnCloseClick">
|
Click="OnCloseClick">
|
||||||
<Button.Styles>
|
<Button.Styles>
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ public partial class ComponentEditorWindow : Window
|
|||||||
"DataLine" => MaterialIconKind.ChartLine,
|
"DataLine" => MaterialIconKind.ChartLine,
|
||||||
"Edit" => MaterialIconKind.Pencil,
|
"Edit" => MaterialIconKind.Pencil,
|
||||||
"Calculator" => MaterialIconKind.Calculator,
|
"Calculator" => MaterialIconKind.Calculator,
|
||||||
|
"Storage" => MaterialIconKind.UsbFlashDrive,
|
||||||
"Globe" => MaterialIconKind.Web,
|
"Globe" => MaterialIconKind.Web,
|
||||||
"Play" => MaterialIconKind.Play,
|
"Play" => MaterialIconKind.Play,
|
||||||
_ => MaterialIconKind.Settings
|
_ => MaterialIconKind.Settings
|
||||||
|
|||||||
@@ -22,14 +22,17 @@
|
|||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
||||||
Classes="component-editor-section-title" />
|
Classes="component-editor-section-title" />
|
||||||
<StackPanel Spacing="8">
|
<ComboBox x:Name="ColorSchemeComboBox"
|
||||||
<RadioButton x:Name="FollowSystemRadioButton"
|
Classes="component-editor-select"
|
||||||
GroupName="ColorScheme"
|
HorizontalAlignment="Stretch"
|
||||||
IsCheckedChanged="OnColorSchemeChanged" />
|
SelectionChanged="OnColorSchemeSelectionChanged">
|
||||||
<RadioButton x:Name="UseNativeRadioButton"
|
<ComboBoxItem x:Name="FollowSystemColorSchemeItem"
|
||||||
GroupName="ColorScheme"
|
Classes="component-editor-select-item"
|
||||||
IsCheckedChanged="OnColorSchemeChanged" />
|
Tag="follow_system" />
|
||||||
</StackPanel>
|
<ComboBoxItem x:Name="UseNativeColorSchemeItem"
|
||||||
|
Classes="component-editor-select-item"
|
||||||
|
Tag="native" />
|
||||||
|
</ComboBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@@ -68,40 +68,37 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
|||||||
var colorSchemeSource = snapshot.ColorSchemeSource;
|
var colorSchemeSource = snapshot.ColorSchemeSource;
|
||||||
|
|
||||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Class Schedule";
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Class Schedule";
|
||||||
DescriptionTextBlock.Text = L("schedule.settings.desc", "导入 ClassIsland 的 CSES 课表文件并选择启用项。");
|
DescriptionTextBlock.Text = L(
|
||||||
|
"schedule.settings.desc",
|
||||||
|
"Import a ClassIsland CSES schedule file and choose which one to use.");
|
||||||
|
|
||||||
ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "配色方案");
|
ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "Color Scheme");
|
||||||
FollowSystemRadioButton.Content = L("component.color_scheme.follow_system", "跟随系统配色");
|
FollowSystemColorSchemeItem.Content = L("component.color_scheme.follow_system", "Follow system color scheme");
|
||||||
UseNativeRadioButton.Content = L("component.color_scheme.native", "使用组件自定义配色");
|
UseNativeColorSchemeItem.Content = L("component.color_scheme.native", "Use component custom color scheme");
|
||||||
|
|
||||||
AddScheduleButton.Content = L("schedule.settings.add", "添加课表");
|
AddScheduleButton.Content = L("schedule.settings.add", "Add Schedule");
|
||||||
EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表");
|
EmptyStateTextBlock.Text = L("schedule.settings.empty", "No imported schedules yet.");
|
||||||
|
|
||||||
_suppressEvents = true;
|
_suppressEvents = true;
|
||||||
|
ColorSchemeComboBox.SelectedItem =
|
||||||
if (string.IsNullOrEmpty(colorSchemeSource) ||
|
string.IsNullOrEmpty(colorSchemeSource) ||
|
||||||
colorSchemeSource == ThemeAppearanceValues.ColorSchemeFollowSystem)
|
string.Equals(colorSchemeSource, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase)
|
||||||
{
|
? FollowSystemColorSchemeItem
|
||||||
FollowSystemRadioButton.IsChecked = true;
|
: UseNativeColorSchemeItem;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UseNativeRadioButton.IsChecked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_suppressEvents = false;
|
_suppressEvents = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnColorSchemeChanged(object? sender, RoutedEventArgs e)
|
private void OnColorSchemeSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
if (_suppressEvents)
|
if (_suppressEvents)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var useNative = UseNativeRadioButton.IsChecked == true;
|
var colorSchemeSource = ColorSchemeComboBox.SelectedItem is ComboBoxItem item && item.Tag is string tag
|
||||||
var colorSchemeSource = useNative
|
? tag
|
||||||
? ThemeAppearanceValues.ColorSchemeNative
|
|
||||||
: ThemeAppearanceValues.ColorSchemeFollowSystem;
|
: ThemeAppearanceValues.ColorSchemeFollowSystem;
|
||||||
|
|
||||||
var snapshot = LoadSnapshot();
|
var snapshot = LoadSnapshot();
|
||||||
@@ -121,11 +118,11 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
|||||||
|
|
||||||
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
{
|
{
|
||||||
Title = L("schedule.settings.picker_title", "选择 ClassIsland 课表文件"),
|
Title = L("schedule.settings.picker_title", "Choose ClassIsland schedule file"),
|
||||||
AllowMultiple = false,
|
AllowMultiple = false,
|
||||||
FileTypeFilter =
|
FileTypeFilter =
|
||||||
[
|
[
|
||||||
new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES 课表"))
|
new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES Schedule"))
|
||||||
{
|
{
|
||||||
Patterns = ["*.cses", "*.yaml", "*.yml"]
|
Patterns = ["*.cses", "*.yaml", "*.yml"]
|
||||||
}
|
}
|
||||||
@@ -155,7 +152,7 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
|||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
DisplayName = Path.GetFileNameWithoutExtension(importedPath)?.Trim()
|
DisplayName = Path.GetFileNameWithoutExtension(importedPath)?.Trim()
|
||||||
?? L("schedule.settings.unnamed", "未命名课表"),
|
?? L("schedule.settings.unnamed", "Untitled Schedule"),
|
||||||
FilePath = importedPath
|
FilePath = importedPath
|
||||||
});
|
});
|
||||||
_activeScheduleId = _importedSchedules[^1].Id;
|
_activeScheduleId = _importedSchedules[^1].Id;
|
||||||
@@ -219,7 +216,7 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
|||||||
var title = new TextBlock
|
var title = new TextBlock
|
||||||
{
|
{
|
||||||
Text = string.IsNullOrWhiteSpace(item.DisplayName)
|
Text = string.IsNullOrWhiteSpace(item.DisplayName)
|
||||||
? L("schedule.settings.unnamed", "未命名课表")
|
? L("schedule.settings.unnamed", "Untitled Schedule")
|
||||||
: item.DisplayName,
|
: item.DisplayName,
|
||||||
FontWeight = FontWeight.SemiBold
|
FontWeight = FontWeight.SemiBold
|
||||||
};
|
};
|
||||||
@@ -234,7 +231,7 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
|||||||
|
|
||||||
var deleteButton = new Button
|
var deleteButton = new Button
|
||||||
{
|
{
|
||||||
Content = L("schedule.settings.delete", "删除"),
|
Content = L("schedule.settings.delete", "Delete"),
|
||||||
Tag = item.Id,
|
Tag = item.Id,
|
||||||
Padding = new Thickness(12, 8),
|
Padding = new Thickness(12, 8),
|
||||||
HorizontalAlignment = HorizontalAlignment.Right
|
HorizontalAlignment = HorizontalAlignment.Right
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<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"
|
||||||
|
x:Class="LanMountainDesktop.Views.ComponentEditors.OfficeRecentDocumentsComponentEditor">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<Border Classes="component-editor-hero-card"
|
||||||
|
Padding="24">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock x:Name="HeadlineTextBlock"
|
||||||
|
Classes="component-editor-headline"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Classes="component-editor-secondary-text"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="component-editor-card"
|
||||||
|
Padding="20">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock x:Name="SourcesHeaderTextBlock"
|
||||||
|
Classes="component-editor-section-title" />
|
||||||
|
<TextBlock x:Name="SourcesDescriptionTextBlock"
|
||||||
|
Classes="component-editor-secondary-text"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<CheckBox x:Name="RegistryCheckBox"
|
||||||
|
IsCheckedChanged="OnSourceSelectionChanged" />
|
||||||
|
<CheckBox x:Name="RecentFoldersCheckBox"
|
||||||
|
IsCheckedChanged="OnSourceSelectionChanged" />
|
||||||
|
<CheckBox x:Name="JumpListsCheckBox"
|
||||||
|
IsCheckedChanged="OnSourceSelectionChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock x:Name="HintTextBlock"
|
||||||
|
Classes="component-editor-secondary-text"
|
||||||
|
Margin="12,0"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||||
|
|
||||||
|
public partial class OfficeRecentDocumentsComponentEditor : ComponentEditorViewBase
|
||||||
|
{
|
||||||
|
private bool _suppressEvents;
|
||||||
|
|
||||||
|
public OfficeRecentDocumentsComponentEditor()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public OfficeRecentDocumentsComponentEditor(DesktopComponentEditorContext? context)
|
||||||
|
: base(context)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ApplyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyState()
|
||||||
|
{
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
var enabledSources = OfficeRecentDocumentSourceTypes.NormalizeValues(
|
||||||
|
snapshot.OfficeRecentDocumentsEnabledSources,
|
||||||
|
useDefaultWhenEmpty: snapshot.OfficeRecentDocumentsEnabledSources is null);
|
||||||
|
|
||||||
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? L(
|
||||||
|
"component.office_recent_documents",
|
||||||
|
"Recent Documents");
|
||||||
|
DescriptionTextBlock.Text = L(
|
||||||
|
"office_recent_documents.settings.desc",
|
||||||
|
"Choose which Windows and Office sources this widget should scan for recent documents.");
|
||||||
|
SourcesHeaderTextBlock.Text = L(
|
||||||
|
"office_recent_documents.settings.sources_title",
|
||||||
|
"Recent document sources");
|
||||||
|
SourcesDescriptionTextBlock.Text = L(
|
||||||
|
"office_recent_documents.settings.sources_desc",
|
||||||
|
"You can combine multiple sources. Registry selection also keeps the Office interop MRU fallback available.");
|
||||||
|
RegistryCheckBox.Content = L(
|
||||||
|
"office_recent_documents.settings.source.registry",
|
||||||
|
"Office registry MRU");
|
||||||
|
RecentFoldersCheckBox.Content = L(
|
||||||
|
"office_recent_documents.settings.source.recent_folders",
|
||||||
|
"Windows Recent folders");
|
||||||
|
JumpListsCheckBox.Content = L(
|
||||||
|
"office_recent_documents.settings.source.jump_lists",
|
||||||
|
"Windows Jump Lists");
|
||||||
|
HintTextBlock.Text = L(
|
||||||
|
"office_recent_documents.settings.hint",
|
||||||
|
"If you disable all sources, this widget will stay empty until at least one source is enabled again.");
|
||||||
|
|
||||||
|
_suppressEvents = true;
|
||||||
|
RegistryCheckBox.IsChecked = enabledSources.Contains(OfficeRecentDocumentSourceTypes.Registry, StringComparer.OrdinalIgnoreCase);
|
||||||
|
RecentFoldersCheckBox.IsChecked = enabledSources.Contains(OfficeRecentDocumentSourceTypes.RecentFolders, StringComparer.OrdinalIgnoreCase);
|
||||||
|
JumpListsCheckBox.IsChecked = enabledSources.Contains(OfficeRecentDocumentSourceTypes.JumpLists, StringComparer.OrdinalIgnoreCase);
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSourceSelectionChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedSources = new[]
|
||||||
|
{
|
||||||
|
RegistryCheckBox.IsChecked == true ? OfficeRecentDocumentSourceTypes.Registry : null,
|
||||||
|
RecentFoldersCheckBox.IsChecked == true ? OfficeRecentDocumentSourceTypes.RecentFolders : null,
|
||||||
|
JumpListsCheckBox.IsChecked == true ? OfficeRecentDocumentSourceTypes.JumpLists : null
|
||||||
|
}
|
||||||
|
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||||
|
.Cast<string>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.OfficeRecentDocumentsEnabledSources = selectedSources;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.OfficeRecentDocumentsEnabledSources));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<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"
|
||||||
|
x:Class="LanMountainDesktop.Views.ComponentEditors.RemovableStorageComponentEditor">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<Border Classes="component-editor-hero-card"
|
||||||
|
Padding="24">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock x:Name="HeadlineTextBlock"
|
||||||
|
Classes="component-editor-headline"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Classes="component-editor-secondary-text"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="component-editor-card"
|
||||||
|
Padding="20">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
||||||
|
Classes="component-editor-section-title" />
|
||||||
|
<ComboBox x:Name="ColorSchemeComboBox"
|
||||||
|
Classes="component-editor-select"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
SelectionChanged="OnColorSchemeSelectionChanged">
|
||||||
|
<ComboBoxItem x:Name="FollowSystemColorSchemeItem"
|
||||||
|
Classes="component-editor-select-item"
|
||||||
|
Tag="follow_system" />
|
||||||
|
<ComboBoxItem x:Name="UseNativeColorSchemeItem"
|
||||||
|
Classes="component-editor-select-item"
|
||||||
|
Tag="native" />
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="component-editor-card"
|
||||||
|
Padding="20">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock x:Name="BehaviorHeaderTextBlock"
|
||||||
|
Classes="component-editor-section-title" />
|
||||||
|
<TextBlock x:Name="BehaviorTextBlock"
|
||||||
|
Classes="component-editor-secondary-text"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||||
|
|
||||||
|
public partial class RemovableStorageComponentEditor : ComponentEditorViewBase
|
||||||
|
{
|
||||||
|
private bool _suppressEvents;
|
||||||
|
|
||||||
|
public RemovableStorageComponentEditor()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemovableStorageComponentEditor(DesktopComponentEditorContext? context)
|
||||||
|
: base(context)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ApplyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyState()
|
||||||
|
{
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
var colorSchemeSource = snapshot.ColorSchemeSource;
|
||||||
|
|
||||||
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Removable Storage";
|
||||||
|
DescriptionTextBlock.Text = L(
|
||||||
|
"removable_storage.settings.desc",
|
||||||
|
"Show a connected USB drive with quick open and eject actions.");
|
||||||
|
ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "Color Scheme");
|
||||||
|
FollowSystemColorSchemeItem.Content = L("component.color_scheme.follow_system", "Follow system color scheme");
|
||||||
|
UseNativeColorSchemeItem.Content = L("component.color_scheme.native", "Use component custom color scheme");
|
||||||
|
BehaviorHeaderTextBlock.Text = L("removable_storage.settings.behavior_title", "Behavior");
|
||||||
|
BehaviorTextBlock.Text = L(
|
||||||
|
"removable_storage.settings.behavior_desc",
|
||||||
|
"The widget automatically watches for removable drives and switches to the newest inserted USB drive.");
|
||||||
|
|
||||||
|
_suppressEvents = true;
|
||||||
|
ColorSchemeComboBox.SelectedItem =
|
||||||
|
string.IsNullOrWhiteSpace(colorSchemeSource) ||
|
||||||
|
string.Equals(colorSchemeSource, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? FollowSystemColorSchemeItem
|
||||||
|
: UseNativeColorSchemeItem;
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnColorSchemeSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ColorSchemeSource = ColorSchemeComboBox.SelectedItem is ComboBoxItem item && item.Tag is string tag
|
||||||
|
? tag
|
||||||
|
: ThemeAppearanceValues.ColorSchemeFollowSystem;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ColorSchemeSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:Class="LanMountainDesktop.Views.ComponentEditors.StudyEnvironmentComponentEditor">
|
x:Class="LanMountainDesktop.Views.ComponentEditors.StudyEnvironmentComponentEditor">
|
||||||
<StackPanel Spacing="16">
|
<StackPanel Spacing="16">
|
||||||
<Border Classes="component-editor-hero_card"
|
<Border Classes="component-editor-hero-card"
|
||||||
Padding="24">
|
Padding="24">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
<TextBlock x:Name="HeadlineTextBlock"
|
<TextBlock x:Name="HeadlineTextBlock"
|
||||||
@@ -23,14 +23,17 @@
|
|||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
||||||
Classes="component-editor-section-title" />
|
Classes="component-editor-section-title" />
|
||||||
<StackPanel Spacing="8">
|
<ComboBox x:Name="ColorSchemeComboBox"
|
||||||
<RadioButton x:Name="FollowSystemRadioButton"
|
Classes="component-editor-select"
|
||||||
GroupName="ColorScheme"
|
HorizontalAlignment="Stretch"
|
||||||
IsCheckedChanged="OnColorSchemeChanged" />
|
SelectionChanged="OnColorSchemeSelectionChanged">
|
||||||
<RadioButton x:Name="UseNativeRadioButton"
|
<ComboBoxItem x:Name="FollowSystemColorSchemeItem"
|
||||||
GroupName="ColorScheme"
|
Classes="component-editor-select-item"
|
||||||
IsCheckedChanged="OnColorSchemeChanged" />
|
Tag="follow_system" />
|
||||||
</StackPanel>
|
<ComboBoxItem x:Name="UseNativeColorSchemeItem"
|
||||||
|
Classes="component-editor-select-item"
|
||||||
|
Tag="native" />
|
||||||
|
</ComboBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Classes="component-editor_card"
|
<Border Classes="component-editor-card"
|
||||||
Padding="20">
|
Padding="20">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<TextBlock x:Name="DbfsHeaderTextBlock"
|
<TextBlock x:Name="DbfsHeaderTextBlock"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
@@ -34,43 +36,40 @@ public partial class StudyEnvironmentComponentEditor : ComponentEditorViewBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Study Environment";
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Study Environment";
|
||||||
DescriptionTextBlock.Text = L("study.environment.settings.desc", "配置右侧实时噪音值显示内容。");
|
DescriptionTextBlock.Text = L(
|
||||||
|
"study.environment.settings.desc",
|
||||||
|
"Configure the realtime audio level information shown on the right side.");
|
||||||
|
|
||||||
ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "配色方案");
|
ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "Color Scheme");
|
||||||
FollowSystemRadioButton.Content = L("component.color_scheme.follow_system", "跟随系统配色");
|
FollowSystemColorSchemeItem.Content = L("component.color_scheme.follow_system", "Follow system color scheme");
|
||||||
UseNativeRadioButton.Content = L("component.color_scheme.native", "使用组件自定义配色");
|
UseNativeColorSchemeItem.Content = L("component.color_scheme.native", "Use component custom color scheme");
|
||||||
|
|
||||||
DisplayDbToggleSwitch.Content = L("study.environment.settings.show_display_db", "显示 display dB");
|
DisplayDbToggleSwitch.Content = L("study.environment.settings.show_display_db", "Show display dB");
|
||||||
DbfsToggleSwitch.Content = L("study.environment.settings.show_dbfs", "显示 dBFS");
|
DbfsToggleSwitch.Content = L("study.environment.settings.show_dbfs", "Show dBFS");
|
||||||
HintTextBlock.Text = L("study.environment.settings.hint", "至少启用一种显示方式。");
|
HintTextBlock.Text = L("study.environment.settings.hint", "At least one display mode must stay enabled.");
|
||||||
|
|
||||||
_suppressEvents = true;
|
_suppressEvents = true;
|
||||||
|
ColorSchemeComboBox.SelectedItem =
|
||||||
if (string.IsNullOrEmpty(colorSchemeSource) ||
|
string.IsNullOrEmpty(colorSchemeSource) ||
|
||||||
colorSchemeSource == ThemeAppearanceValues.ColorSchemeFollowSystem)
|
string.Equals(colorSchemeSource, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase)
|
||||||
{
|
? FollowSystemColorSchemeItem
|
||||||
FollowSystemRadioButton.IsChecked = true;
|
: UseNativeColorSchemeItem;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UseNativeRadioButton.IsChecked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DisplayDbToggleSwitch.IsChecked = showDisplayDb;
|
DisplayDbToggleSwitch.IsChecked = showDisplayDb;
|
||||||
DbfsToggleSwitch.IsChecked = showDbfs;
|
DbfsToggleSwitch.IsChecked = showDbfs;
|
||||||
_suppressEvents = false;
|
_suppressEvents = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnColorSchemeChanged(object? sender, RoutedEventArgs e)
|
private void OnColorSchemeSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
if (_suppressEvents)
|
if (_suppressEvents)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var useNative = UseNativeRadioButton.IsChecked == true;
|
var colorSchemeSource = ColorSchemeComboBox.SelectedItem is ComboBoxItem item && item.Tag is string tag
|
||||||
var colorSchemeSource = useNative
|
? tag
|
||||||
? ThemeAppearanceValues.ColorSchemeNative
|
|
||||||
: ThemeAppearanceValues.ColorSchemeFollowSystem;
|
: ThemeAppearanceValues.ColorSchemeFollowSystem;
|
||||||
|
|
||||||
var snapshot = LoadSnapshot();
|
var snapshot = LoadSnapshot();
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<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"
|
||||||
|
x:Class="LanMountainDesktop.Views.ComponentEditors.WhiteboardComponentEditor">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<Border Classes="component-editor-hero-card"
|
||||||
|
Padding="24">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock x:Name="HeadlineTextBlock"
|
||||||
|
Classes="component-editor-headline"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Classes="component-editor-secondary-text"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="component-editor-card"
|
||||||
|
Padding="20">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock x:Name="RetentionHeaderTextBlock"
|
||||||
|
Classes="component-editor-section-title" />
|
||||||
|
<TextBlock x:Name="RetentionDescriptionTextBlock"
|
||||||
|
Classes="component-editor-secondary-text"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<ComboBox x:Name="RetentionComboBox"
|
||||||
|
Classes="component-editor-select"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
SelectionChanged="OnRetentionSelectionChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock x:Name="InstanceHintTextBlock"
|
||||||
|
Classes="component-editor-secondary-text"
|
||||||
|
Margin="12,0"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||||
|
|
||||||
|
public partial class WhiteboardComponentEditor : ComponentEditorViewBase
|
||||||
|
{
|
||||||
|
private bool _suppressEvents;
|
||||||
|
|
||||||
|
public WhiteboardComponentEditor()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public WhiteboardComponentEditor(DesktopComponentEditorContext? context)
|
||||||
|
: base(context)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
BuildRetentionOptions();
|
||||||
|
ApplyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildRetentionOptions()
|
||||||
|
{
|
||||||
|
RetentionComboBox.Items.Clear();
|
||||||
|
for (var days = WhiteboardNoteRetentionPolicy.MinimumDays; days <= WhiteboardNoteRetentionPolicy.MaximumDays; days++)
|
||||||
|
{
|
||||||
|
var item = new ComboBoxItem
|
||||||
|
{
|
||||||
|
Tag = days.ToString(),
|
||||||
|
Content = L(
|
||||||
|
"whiteboard.settings.retention.option",
|
||||||
|
"{0} days").Replace("{0}", days.ToString())
|
||||||
|
};
|
||||||
|
item.Classes.Add("component-editor-select-item");
|
||||||
|
RetentionComboBox.Items.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyState()
|
||||||
|
{
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
var retentionDays = NormalizeRetentionDays(snapshot.WhiteboardNoteRetentionDays);
|
||||||
|
|
||||||
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Blackboard";
|
||||||
|
DescriptionTextBlock.Text = L(
|
||||||
|
"whiteboard.settings.desc",
|
||||||
|
"Each blackboard keeps its own note history and saves it independently.");
|
||||||
|
RetentionHeaderTextBlock.Text = L(
|
||||||
|
"whiteboard.settings.retention.title",
|
||||||
|
"Note retention");
|
||||||
|
RetentionDescriptionTextBlock.Text = L(
|
||||||
|
"whiteboard.settings.retention.desc",
|
||||||
|
"Choose how long this blackboard should keep saved notes before expired data is removed automatically.");
|
||||||
|
InstanceHintTextBlock.Text = L(
|
||||||
|
"whiteboard.settings.instance_scope",
|
||||||
|
"This retention setting is stored per blackboard component instance.");
|
||||||
|
|
||||||
|
_suppressEvents = true;
|
||||||
|
RetentionComboBox.SelectedItem = RetentionComboBox.Items
|
||||||
|
.OfType<ComboBoxItem>()
|
||||||
|
.FirstOrDefault(item =>
|
||||||
|
item.Tag is string tag &&
|
||||||
|
int.TryParse(tag, out var days) &&
|
||||||
|
days == retentionDays);
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRetentionSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.WhiteboardNoteRetentionDays = GetSelectedRetentionDays();
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.WhiteboardNoteRetentionDays));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetSelectedRetentionDays()
|
||||||
|
{
|
||||||
|
if (RetentionComboBox.SelectedItem is ComboBoxItem item &&
|
||||||
|
item.Tag is string tag &&
|
||||||
|
int.TryParse(tag, out var days))
|
||||||
|
{
|
||||||
|
return NormalizeRetentionDays(days);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WhiteboardNoteRetentionPolicy.DefaultDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NormalizeRetentionDays(int days)
|
||||||
|
{
|
||||||
|
return WhiteboardNoteRetentionPolicy.NormalizeDays(
|
||||||
|
days <= 0
|
||||||
|
? WhiteboardNoteRetentionPolicy.DefaultDays
|
||||||
|
: days);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
<Grid Grid.Row="1"
|
<Grid Grid.Row="1"
|
||||||
ColumnDefinitions="240,*"
|
ColumnDefinitions="240,*"
|
||||||
ColumnSpacing="12">
|
ColumnSpacing="12">
|
||||||
<Border Classes="glass-panel"
|
<Border Classes="surface-translucent-panel"
|
||||||
CornerRadius="24"
|
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||||
Padding="10">
|
Padding="10">
|
||||||
<ListBox x:Name="CategoryListBox"
|
<ListBox x:Name="CategoryListBox"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
|
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
|
||||||
<Border Padding="10"
|
<Border Padding="10"
|
||||||
Margin="0,0,0,6"
|
Margin="0,0,0,6"
|
||||||
CornerRadius="14"
|
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
||||||
Background="{DynamicResource AdaptiveNavItemBackgroundBrush}">
|
Background="{DynamicResource AdaptiveNavItemBackgroundBrush}">
|
||||||
<Grid ColumnDefinitions="Auto,*"
|
<Grid ColumnDefinitions="Auto,*"
|
||||||
ColumnSpacing="8">
|
ColumnSpacing="8">
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Column="1"
|
<Border Grid.Column="1"
|
||||||
Classes="glass-strong"
|
Classes="surface-translucent-strong"
|
||||||
CornerRadius="24"
|
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||||
Padding="10">
|
Padding="10">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
@@ -87,14 +87,14 @@
|
|||||||
<Border Width="240"
|
<Border Width="240"
|
||||||
Height="220"
|
Height="220"
|
||||||
Margin="6"
|
Margin="6"
|
||||||
CornerRadius="18"
|
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||||
Padding="10"
|
Padding="10"
|
||||||
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||||
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||||
BorderThickness="1">
|
BorderThickness="1">
|
||||||
<Grid RowDefinitions="*,Auto,Auto"
|
<Grid RowDefinitions="*,Auto,Auto"
|
||||||
RowSpacing="8">
|
RowSpacing="8">
|
||||||
<Border CornerRadius="12"
|
<Border CornerRadius="{DynamicResource DesignCornerRadiusXs}"
|
||||||
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
|
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(42 * scale, 16, 56));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(42 * scale, 16, 56);
|
||||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 26));
|
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 26));
|
||||||
ApplyModeVisualIfNeeded();
|
ApplyModeVisualIfNeeded();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,12 +381,12 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
|||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * softScale, 16, 52));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||||
RootBorder.Padding = new Thickness(0);
|
RootBorder.Padding = new Thickness(0);
|
||||||
|
|
||||||
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
||||||
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
||||||
CardBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * softScale, 16, 52));
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||||
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||||
|
|
||||||
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
||||||
|
|||||||
@@ -386,12 +386,12 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
|||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * softScale, 16, 52));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||||
RootBorder.Padding = new Thickness(0);
|
RootBorder.Padding = new Thickness(0);
|
||||||
|
|
||||||
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
var horizontalPadding = Math.Clamp(16 * softScale, 8, 24);
|
||||||
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
var verticalPadding = Math.Clamp(14 * softScale, 7, 20);
|
||||||
CardBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * softScale, 16, 52));
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52);
|
||||||
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||||
|
|
||||||
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d));
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
|||||||
{
|
{
|
||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.34, 12, 28));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 12, 28);
|
||||||
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18));
|
RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18));
|
||||||
|
|
||||||
WebViewHostBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.24, 10, 22));
|
WebViewHostBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.24, 10, 22);
|
||||||
AddressBarBorder.CornerRadius = new CornerRadius(Math.Clamp(_currentCellSize * 0.22, 10, 20));
|
AddressBarBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.22, 10, 20);
|
||||||
AddressBarBorder.Padding = new Thickness(8, 6);
|
AddressBarBorder.Padding = new Thickness(8, 6);
|
||||||
|
|
||||||
if (RootBorder.Child is Grid rootGrid)
|
if (RootBorder.Child is Grid rootGrid)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
<ScrollViewer x:Name="ContentScrollViewer"
|
<ScrollViewer x:Name="ContentScrollViewer"
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
VerticalScrollBarVisibility="Disabled">
|
VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel x:Name="CourseListPanel" />
|
<StackPanel x:Name="CourseListPanel" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
|||||||
@@ -198,12 +198,32 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (courseIndex < CourseListPanel.Children.Count)
|
// 确保在UI线程执行
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
var targetChild = CourseListPanel.Children[courseIndex];
|
if (courseIndex >= CourseListPanel.Children.Count)
|
||||||
var bounds = targetChild.Bounds;
|
{
|
||||||
ContentScrollViewer.Offset = new Vector(0, bounds.Position.Y);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var targetChild = CourseListPanel.Children[courseIndex];
|
||||||
|
if (targetChild == null || !targetChild.IsArrangeValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = targetChild.Bounds;
|
||||||
|
var scrollViewerHeight = ContentScrollViewer.Bounds.Height;
|
||||||
|
var contentHeight = CourseListPanel.Bounds.Height;
|
||||||
|
|
||||||
|
// 计算滚动位置,使当前课程居中显示
|
||||||
|
var targetOffset = bounds.Position.Y - (scrollViewerHeight / 2) + (bounds.Height / 2);
|
||||||
|
|
||||||
|
// 确保不超出边界
|
||||||
|
targetOffset = Math.Max(0, Math.Min(targetOffset, contentHeight - scrollViewerHeight));
|
||||||
|
|
||||||
|
ContentScrollViewer.Offset = new Vector(0, targetOffset);
|
||||||
|
}, Avalonia.Threading.DispatcherPriority.Loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshFromSettings()
|
public void RefreshFromSettings()
|
||||||
@@ -298,6 +318,15 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var currentIndex = FindCurrentCourseIndex();
|
var currentIndex = FindCurrentCourseIndex();
|
||||||
_lastCurrentCourseIndex = currentIndex;
|
_lastCurrentCourseIndex = currentIndex;
|
||||||
HideStatus();
|
HideStatus();
|
||||||
|
|
||||||
|
// 初始化时自动跳转到当前课程
|
||||||
|
if (currentIndex >= 0)
|
||||||
|
{
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
ScrollToCurrentCourse(currentIndex);
|
||||||
|
}, Avalonia.Threading.DispatcherPriority.Loaded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderScheduleItems();
|
RenderScheduleItems();
|
||||||
@@ -484,10 +513,9 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
: CreateBrush("#FF4D5A");
|
: CreateBrush("#FF4D5A");
|
||||||
var normalBulletBrush = CreateBrush(_isNightVisual ? "#B8BEC9" : "#9AA3B2");
|
var normalBulletBrush = CreateBrush(_isNightVisual ? "#B8BEC9" : "#9AA3B2");
|
||||||
|
|
||||||
var visibleItems = _courseItems.Take(maxVisibleItems).ToList();
|
for (var i = 0; i < _courseItems.Count; i++)
|
||||||
for (var i = 0; i < visibleItems.Count; i++)
|
|
||||||
{
|
{
|
||||||
var item = visibleItems[i];
|
var item = _courseItems[i];
|
||||||
var bulletBrush = item.IsCurrent ? currentBrush : normalBulletBrush;
|
var bulletBrush = item.IsCurrent ? currentBrush : normalBulletBrush;
|
||||||
|
|
||||||
var bullet = new Border
|
var bullet = new Border
|
||||||
@@ -585,18 +613,17 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
? CreateBrush("#FF4FC3F7")
|
? CreateBrush("#FF4FC3F7")
|
||||||
: CreateBrush("#FF3250");
|
: CreateBrush("#FF3250");
|
||||||
|
|
||||||
var cornerRadius = Math.Clamp(_currentCellSize * 0.45, 24, 44);
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.45, 24, 44);
|
||||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
|
||||||
RootBorder.Background = _isNightVisual
|
RootBorder.Background = _isNightVisual
|
||||||
? CreateGradientBrush("#171A21", "#0C0E14")
|
? CreateGradientBrush("#171A21", "#0C0E14")
|
||||||
: CreateGradientBrush("#F7F8FC", "#ECEFF6");
|
: CreateGradientBrush("#F7F8FC", "#ECEFF6");
|
||||||
RootBorder.BorderBrush = CreateBrush(_isNightVisual ? "#24FFFFFF" : "#15000000");
|
RootBorder.BorderBrush = CreateBrush(_isNightVisual ? "#24FFFFFF" : "#15000000");
|
||||||
|
|
||||||
var rootPadding = new Thickness(
|
var rootPadding = new Thickness(
|
||||||
Math.Clamp(16 * scale, 10, 24),
|
ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 10, 24),
|
||||||
Math.Clamp(14 * scale, 9, 20),
|
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 9, 20),
|
||||||
Math.Clamp(16 * scale, 10, 24),
|
ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 10, 24),
|
||||||
Math.Clamp(14 * scale, 8, 20));
|
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 20));
|
||||||
RootBorder.Padding = rootPadding;
|
RootBorder.Padding = rootPadding;
|
||||||
|
|
||||||
LayoutGrid.RowSpacing = Math.Clamp(14 * scale, 6, 20);
|
LayoutGrid.RowSpacing = Math.Clamp(14 * scale, 6, 20);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
x:Class="LanMountainDesktop.Views.Components.ClockWidget">
|
x:Class="LanMountainDesktop.Views.Components.ClockWidget">
|
||||||
|
|
||||||
<Border x:Name="RootBorder"
|
<Border x:Name="RootBorder"
|
||||||
Classes="glass-panel"
|
Classes="surface-translucent-panel"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
CornerRadius="24">
|
CornerRadius="24">
|
||||||
<StackPanel Orientation="Horizontal"
|
<StackPanel Orientation="Horizontal"
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
|||||||
|
|
||||||
private TimeZoneService? _timeZoneService;
|
private TimeZoneService? _timeZoneService;
|
||||||
private ClockDisplayFormat _displayFormat = ClockDisplayFormat.HourMinuteSecond;
|
private ClockDisplayFormat _displayFormat = ClockDisplayFormat.HourMinuteSecond;
|
||||||
|
private bool _transparentBackground;
|
||||||
|
private double _lastAppliedCellSize = 100;
|
||||||
|
|
||||||
public ClockWidget()
|
public ClockWidget()
|
||||||
{
|
{
|
||||||
@@ -44,11 +46,32 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TransparentBackground
|
||||||
|
{
|
||||||
|
get => _transparentBackground;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_transparentBackground == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_transparentBackground = value;
|
||||||
|
ApplyChrome();
|
||||||
|
ApplyCellSize(_lastAppliedCellSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void SetDisplayFormat(ClockDisplayFormat format)
|
public void SetDisplayFormat(ClockDisplayFormat format)
|
||||||
{
|
{
|
||||||
DisplayFormat = format;
|
DisplayFormat = format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetTransparentBackground(bool transparentBackground)
|
||||||
|
{
|
||||||
|
TransparentBackground = transparentBackground;
|
||||||
|
}
|
||||||
|
|
||||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||||
{
|
{
|
||||||
ClearTimeZoneService();
|
ClearTimeZoneService();
|
||||||
@@ -101,6 +124,8 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
|||||||
|
|
||||||
public void ApplyCellSize(double cellSize)
|
public void ApplyCellSize(double cellSize)
|
||||||
{
|
{
|
||||||
|
_lastAppliedCellSize = cellSize;
|
||||||
|
|
||||||
// --- Class Island “满盈”风格算法 ---
|
// --- Class Island “满盈”风格算法 ---
|
||||||
|
|
||||||
// 1. 计算组件高度:保持与任务栏核心比例一致 (0.74x)
|
// 1. 计算组件高度:保持与任务栏核心比例一致 (0.74x)
|
||||||
@@ -130,7 +155,38 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo
|
|||||||
panel.Spacing = Math.Clamp(cellSize * 0.06, 2, 8);
|
panel.Spacing = Math.Clamp(cellSize * 0.06, 2, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_transparentBackground)
|
||||||
|
{
|
||||||
|
RootBorder.MinWidth = 0;
|
||||||
|
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 4, 10), 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 确保清除可能存在的固定 Padding,由代码控制“紧密感”
|
// 确保清除可能存在的固定 Padding,由代码控制“紧密感”
|
||||||
|
RootBorder.MinWidth = cellSize * 2.2;
|
||||||
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0);
|
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyChrome()
|
||||||
|
{
|
||||||
|
if (_transparentBackground)
|
||||||
|
{
|
||||||
|
RootBorder.Classes.Remove("glass-panel");
|
||||||
|
RootBorder.Background = Brushes.Transparent;
|
||||||
|
RootBorder.BorderBrush = Brushes.Transparent;
|
||||||
|
RootBorder.BorderThickness = new Thickness(0);
|
||||||
|
RootBorder.BoxShadow = default;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RootBorder.Classes.Contains("glass-panel"))
|
||||||
|
{
|
||||||
|
RootBorder.Classes.Add("glass-panel");
|
||||||
|
}
|
||||||
|
|
||||||
|
RootBorder.ClearValue(Border.BackgroundProperty);
|
||||||
|
RootBorder.ClearValue(Border.BorderBrushProperty);
|
||||||
|
RootBorder.ClearValue(Border.BorderThicknessProperty);
|
||||||
|
RootBorder.ClearValue(Border.BoxShadowProperty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
{
|
{
|
||||||
Width = 160,
|
Width = 160,
|
||||||
Height = 90,
|
Height = 90,
|
||||||
CornerRadius = new CornerRadius(16),
|
CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16, 8, 22),
|
||||||
ClipToBounds = true,
|
ClipToBounds = true,
|
||||||
Background = new SolidColorBrush(Color.Parse("#E6E6E6")),
|
Background = new SolidColorBrush(Color.Parse("#E6E6E6")),
|
||||||
IsHitTestVisible = false
|
IsHitTestVisible = false
|
||||||
@@ -545,10 +545,10 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
RootBorder.Padding = new Thickness(0);
|
RootBorder.Padding = new Thickness(0);
|
||||||
|
|
||||||
CardBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52));
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
CardBorder.Padding = new Thickness(
|
CardBorder.Padding = new Thickness(
|
||||||
Math.Clamp(16 * scale, 8, 24),
|
Math.Clamp(16 * scale, 8, 24),
|
||||||
Math.Clamp(14 * scale, 7, 22),
|
Math.Clamp(14 * scale, 7, 22),
|
||||||
@@ -573,8 +573,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
News1ImageHost.Height = imageHeight;
|
News1ImageHost.Height = imageHeight;
|
||||||
News2ImageHost.Width = imageWidth;
|
News2ImageHost.Width = imageWidth;
|
||||||
News2ImageHost.Height = imageHeight;
|
News2ImageHost.Height = imageHeight;
|
||||||
News1ImageHost.CornerRadius = new CornerRadius(Math.Clamp(16 * scale, 8, 22));
|
News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
|
||||||
News2ImageHost.CornerRadius = new CornerRadius(Math.Clamp(16 * scale, 8, 22));
|
News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
|
||||||
|
|
||||||
var columnGap = Math.Clamp(12 * scale, 6, 18);
|
var columnGap = Math.Clamp(12 * scale, 6, 18);
|
||||||
NewsItem1Grid.ColumnSpacing = columnGap;
|
NewsItem1Grid.ColumnSpacing = columnGap;
|
||||||
@@ -611,7 +611,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
row.ImageHost.Width = imageWidth;
|
row.ImageHost.Width = imageWidth;
|
||||||
row.ImageHost.Height = imageHeight;
|
row.ImageHost.Height = imageHeight;
|
||||||
row.ImageHost.CornerRadius = new CornerRadius(Math.Clamp(16 * scale, 8, 22));
|
row.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
|
||||||
|
|
||||||
row.TitleTextBlock.MaxWidth = availableTextWidth;
|
row.TitleTextBlock.MaxWidth = availableTextWidth;
|
||||||
row.TitleTextBlock.FontSize = Math.Clamp(19 * scale, 10, 25);
|
row.TitleTextBlock.FontSize = Math.Clamp(19 * scale, 10, 25);
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
internal static class ComponentChromeCornerRadiusHelper
|
||||||
|
{
|
||||||
|
public static double ResolveScale(ComponentChromeContext? chromeContext = null)
|
||||||
|
{
|
||||||
|
if (chromeContext is not null)
|
||||||
|
{
|
||||||
|
return Math.Max(0.1d, chromeContext.GlobalCornerRadiusScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Max(0.1d, HostAppearanceThemeProvider.GetOrCreate().GetCurrent().GlobalCornerRadiusScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CornerRadius Scale(double baseRadius, double min, double max, ComponentChromeContext? chromeContext = null)
|
||||||
|
{
|
||||||
|
var scale = ResolveScale(chromeContext);
|
||||||
|
return new CornerRadius(Math.Clamp(baseRadius * scale, min * scale, max * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Apply(CornerRadius radius, params Border?[] chromeLayers)
|
||||||
|
{
|
||||||
|
foreach (var chromeLayer in chromeLayers)
|
||||||
|
{
|
||||||
|
if (chromeLayer is not null)
|
||||||
|
{
|
||||||
|
chromeLayer.CornerRadius = radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CornerRadius ResolveToken(string key, double fallback)
|
||||||
|
{
|
||||||
|
var application = Application.Current;
|
||||||
|
return application is not null &&
|
||||||
|
application.Resources.TryGetResource(key, application.ActualThemeVariant, out var resource) &&
|
||||||
|
resource is CornerRadius radius
|
||||||
|
? radius
|
||||||
|
: new CornerRadius(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double ScaleValue(double value, ComponentChromeContext? chromeContext = null)
|
||||||
|
{
|
||||||
|
return value * ResolveScale(chromeContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double ResolveContentSafetyScale(
|
||||||
|
ComponentChromeContext? chromeContext = null,
|
||||||
|
double responsiveness = 0.45d)
|
||||||
|
{
|
||||||
|
var scale = ResolveScale(chromeContext);
|
||||||
|
var normalizedResponsiveness = Math.Clamp(responsiveness, 0d, 1d);
|
||||||
|
return 1d + ((scale - 1d) * normalizedResponsiveness);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double SafeValue(
|
||||||
|
double baseValue,
|
||||||
|
double min,
|
||||||
|
double max,
|
||||||
|
ComponentChromeContext? chromeContext = null,
|
||||||
|
double responsiveness = 0.45d)
|
||||||
|
{
|
||||||
|
var safetyScale = ResolveContentSafetyScale(chromeContext, responsiveness);
|
||||||
|
return Math.Clamp(baseValue * safetyScale, min * safetyScale, max * safetyScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
|||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
|
|
||||||
InfoPanel.Padding = new Thickness(
|
InfoPanel.Padding = new Thickness(
|
||||||
Math.Clamp(18 * scale, 10, 28),
|
Math.Clamp(18 * scale, 10, 28),
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
|||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
RootBorder.Padding = new Thickness(
|
RootBorder.Padding = new Thickness(
|
||||||
Math.Clamp(20 * scale, 10, 34),
|
Math.Clamp(20 * scale, 10, 34),
|
||||||
Math.Clamp(16 * scale, 8, 28),
|
Math.Clamp(16 * scale, 8, 28),
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
|||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(30 * scale, 14, 40));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 14, 40);
|
||||||
CardBorder.CornerRadius = RootBorder.CornerRadius;
|
CardBorder.CornerRadius = RootBorder.CornerRadius;
|
||||||
CardBorder.Padding = new Thickness(
|
CardBorder.Padding = new Thickness(
|
||||||
Math.Clamp(12 * scale, 8, 18),
|
Math.Clamp(12 * scale, 8, 18),
|
||||||
|
|||||||
@@ -298,10 +298,11 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
isFourByThree = widthRatio >= 0.9 && heightRatio >= 1.35;
|
isFourByThree = widthRatio >= 0.9 && heightRatio >= 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52));
|
var containerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52);
|
||||||
|
RootBorder.CornerRadius = containerRadius;
|
||||||
RootBorder.Padding = new Thickness(0);
|
RootBorder.Padding = new Thickness(0);
|
||||||
|
|
||||||
CardBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52));
|
CardBorder.CornerRadius = containerRadius;
|
||||||
CardBorder.Padding = new Thickness(
|
CardBorder.Padding = new Thickness(
|
||||||
Math.Clamp(16 * scale, 8, 24),
|
Math.Clamp(16 * scale, 8, 24),
|
||||||
Math.Clamp(14 * scale, 7, 22),
|
Math.Clamp(14 * scale, 7, 22),
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
|||||||
{
|
{
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(28 * scale, 16, 40));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 16, 40);
|
||||||
RootBorder.Padding = new Thickness(Math.Clamp(11 * scale, 7, 17));
|
RootBorder.Padding = new Thickness(Math.Clamp(11 * scale, 7, 17));
|
||||||
|
|
||||||
LayoutRoot.ColumnSpacing = Math.Clamp(10 * scale, 6, 16);
|
LayoutRoot.ColumnSpacing = Math.Clamp(10 * scale, 6, 16);
|
||||||
@@ -337,7 +337,7 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon
|
|||||||
Math.Clamp(2.4 * scale, 1, 4));
|
Math.Clamp(2.4 * scale, 1, 4));
|
||||||
CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(0.8 * scale, 0, 2));
|
CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(0.8 * scale, 0, 2));
|
||||||
|
|
||||||
LunarCardBorder.CornerRadius = new CornerRadius(Math.Clamp(24 * scale, 14, 34));
|
LunarCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 14, 34);
|
||||||
LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 20));
|
LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 20));
|
||||||
RightPanelGrid.RowSpacing = Math.Clamp(7.5 * scale, 3.5, 11);
|
RightPanelGrid.RowSpacing = Math.Clamp(7.5 * scale, 3.5, 11);
|
||||||
DividerBorder.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 2), 0, Math.Clamp(1 * scale, 0, 2));
|
DividerBorder.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 2), 0, Math.Clamp(1 * scale, 0, 2));
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using LanMountainDesktop.Appearance;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
@@ -30,7 +32,11 @@ public sealed class DesktopComponentRuntimeRegistration
|
|||||||
string? displayNameLocalizationKey,
|
string? displayNameLocalizationKey,
|
||||||
Func<Control> controlFactory,
|
Func<Control> controlFactory,
|
||||||
Func<double, double>? cornerRadiusResolver = null)
|
Func<double, double>? cornerRadiusResolver = null)
|
||||||
: this(componentId, displayNameLocalizationKey, _ => controlFactory(), cornerRadiusResolver)
|
: this(
|
||||||
|
componentId,
|
||||||
|
displayNameLocalizationKey,
|
||||||
|
_ => controlFactory(),
|
||||||
|
cornerRadiusResolver is null ? null : chromeContext => cornerRadiusResolver(chromeContext.CellSize))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +45,19 @@ public sealed class DesktopComponentRuntimeRegistration
|
|||||||
string? displayNameLocalizationKey,
|
string? displayNameLocalizationKey,
|
||||||
Func<DesktopComponentControlFactoryContext, Control> controlFactory,
|
Func<DesktopComponentControlFactoryContext, Control> controlFactory,
|
||||||
Func<double, double>? cornerRadiusResolver = null)
|
Func<double, double>? cornerRadiusResolver = null)
|
||||||
|
: this(
|
||||||
|
componentId,
|
||||||
|
displayNameLocalizationKey,
|
||||||
|
controlFactory,
|
||||||
|
cornerRadiusResolver is null ? null : chromeContext => cornerRadiusResolver(chromeContext.CellSize))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DesktopComponentRuntimeRegistration(
|
||||||
|
string componentId,
|
||||||
|
string? displayNameLocalizationKey,
|
||||||
|
Func<DesktopComponentControlFactoryContext, Control> controlFactory,
|
||||||
|
Func<ComponentChromeContext, double>? cornerRadiusResolver = null)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
|
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
|
||||||
ArgumentNullException.ThrowIfNull(controlFactory);
|
ArgumentNullException.ThrowIfNull(controlFactory);
|
||||||
@@ -57,22 +76,27 @@ public sealed class DesktopComponentRuntimeRegistration
|
|||||||
|
|
||||||
public Func<DesktopComponentControlFactoryContext, Control> ControlFactory { get; }
|
public Func<DesktopComponentControlFactoryContext, Control> ControlFactory { get; }
|
||||||
|
|
||||||
public Func<double, double>? CornerRadiusResolver { get; }
|
public Func<ComponentChromeContext, double>? CornerRadiusResolver { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DesktopComponentRuntimeDescriptor
|
public sealed class DesktopComponentRuntimeDescriptor
|
||||||
{
|
{
|
||||||
private static readonly Func<double, double> DefaultCornerRadiusResolver =
|
private static readonly Func<ComponentChromeContext, double> DefaultCornerRadiusResolver =
|
||||||
cellSize => Math.Clamp(cellSize * 0.22, 8, 18);
|
chromeContext =>
|
||||||
|
{
|
||||||
|
var scale = Math.Max(0.1d, chromeContext.GlobalCornerRadiusScale);
|
||||||
|
var baseRadius = Math.Clamp(chromeContext.CellSize * 0.22, 8, 18);
|
||||||
|
return Math.Clamp(baseRadius * scale, 8 * scale, 18 * scale);
|
||||||
|
};
|
||||||
|
|
||||||
private readonly Func<DesktopComponentControlFactoryContext, Control> _controlFactory;
|
private readonly Func<DesktopComponentControlFactoryContext, Control> _controlFactory;
|
||||||
private readonly Func<double, double> _cornerRadiusResolver;
|
private readonly Func<ComponentChromeContext, double> _cornerRadiusResolver;
|
||||||
|
|
||||||
internal DesktopComponentRuntimeDescriptor(
|
internal DesktopComponentRuntimeDescriptor(
|
||||||
DesktopComponentDefinition definition,
|
DesktopComponentDefinition definition,
|
||||||
string? displayNameLocalizationKey,
|
string? displayNameLocalizationKey,
|
||||||
Func<DesktopComponentControlFactoryContext, Control> controlFactory,
|
Func<DesktopComponentControlFactoryContext, Control> controlFactory,
|
||||||
Func<double, double>? cornerRadiusResolver)
|
Func<ComponentChromeContext, double>? cornerRadiusResolver)
|
||||||
{
|
{
|
||||||
Definition = definition;
|
Definition = definition;
|
||||||
DisplayNameLocalizationKey = displayNameLocalizationKey;
|
DisplayNameLocalizationKey = displayNameLocalizationKey;
|
||||||
@@ -97,9 +121,16 @@ public sealed class DesktopComponentRuntimeDescriptor
|
|||||||
|
|
||||||
var settingsService = settingsFacade.Settings;
|
var settingsService = settingsFacade.Settings;
|
||||||
var appearanceTheme = HostAppearanceThemeProvider.GetOrCreate();
|
var appearanceTheme = HostAppearanceThemeProvider.GetOrCreate();
|
||||||
|
var appearanceSnapshot = appearanceTheme.GetCurrent();
|
||||||
var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId);
|
var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId);
|
||||||
var componentSettingsStore = new ComponentSettingsService(settingsService);
|
var componentSettingsStore = new ComponentSettingsService(settingsService);
|
||||||
componentSettingsStore.SetScopedComponentContext(Definition.Id, placementId);
|
componentSettingsStore.SetScopedComponentContext(Definition.Id, placementId);
|
||||||
|
var chromeContext = new ComponentChromeContext(
|
||||||
|
Definition.Id,
|
||||||
|
placementId,
|
||||||
|
cellSize,
|
||||||
|
appearanceSnapshot.GlobalCornerRadiusScale,
|
||||||
|
appearanceSnapshot.CornerRadiusTokens);
|
||||||
var control = _controlFactory(new DesktopComponentControlFactoryContext(
|
var control = _controlFactory(new DesktopComponentControlFactoryContext(
|
||||||
Definition,
|
Definition,
|
||||||
cellSize,
|
cellSize,
|
||||||
@@ -118,6 +149,7 @@ public sealed class DesktopComponentRuntimeDescriptor
|
|||||||
settingsFacade,
|
settingsFacade,
|
||||||
settingsService,
|
settingsService,
|
||||||
appearanceTheme,
|
appearanceTheme,
|
||||||
|
chromeContext,
|
||||||
componentAccessor,
|
componentAccessor,
|
||||||
componentSettingsStore);
|
componentSettingsStore);
|
||||||
|
|
||||||
@@ -145,6 +177,11 @@ public sealed class DesktopComponentRuntimeDescriptor
|
|||||||
placementAwareComponent.SetComponentPlacementContext(Definition.Id, placementId);
|
placementAwareComponent.SetComponentPlacementContext(Definition.Id, placementId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (control is IComponentChromeContextAware chromeContextAwareComponent)
|
||||||
|
{
|
||||||
|
chromeContextAwareComponent.SetComponentChromeContext(chromeContext);
|
||||||
|
}
|
||||||
|
|
||||||
if (control is IDesktopComponentWidget sizedComponent)
|
if (control is IDesktopComponentWidget sizedComponent)
|
||||||
{
|
{
|
||||||
sizedComponent.ApplyCellSize(cellSize);
|
sizedComponent.ApplyCellSize(cellSize);
|
||||||
@@ -173,9 +210,22 @@ public sealed class DesktopComponentRuntimeDescriptor
|
|||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double ResolveCornerRadius(ComponentChromeContext chromeContext)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(chromeContext);
|
||||||
|
|
||||||
|
var resolved = _cornerRadiusResolver(chromeContext with { CellSize = Math.Max(1, chromeContext.CellSize) });
|
||||||
|
return double.IsFinite(resolved) ? Math.Max(0d, resolved) : DefaultCornerRadiusResolver(chromeContext);
|
||||||
|
}
|
||||||
|
|
||||||
public double ResolveCornerRadius(double cellSize)
|
public double ResolveCornerRadius(double cellSize)
|
||||||
{
|
{
|
||||||
return _cornerRadiusResolver(Math.Max(1, cellSize));
|
return ResolveCornerRadius(new ComponentChromeContext(
|
||||||
|
Definition.Id,
|
||||||
|
null,
|
||||||
|
Math.Max(1, cellSize),
|
||||||
|
1d,
|
||||||
|
AppearanceCornerRadiusTokenFactory.Create(1d)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplySettingsDependencies(
|
private static void ApplySettingsDependencies(
|
||||||
@@ -444,6 +494,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
|||||||
"component.office_recent_documents",
|
"component.office_recent_documents",
|
||||||
() => new OfficeRecentDocumentsWidget(),
|
() => new OfficeRecentDocumentsWidget(),
|
||||||
cellSize => Math.Clamp(cellSize * 0.50, 10, 24)),
|
cellSize => Math.Clamp(cellSize * 0.50, 10, 24)),
|
||||||
|
new DesktopComponentRuntimeRegistration(
|
||||||
|
BuiltInComponentIds.DesktopRemovableStorage,
|
||||||
|
"component.removable_storage",
|
||||||
|
() => new RemovableStorageWidget(),
|
||||||
|
cellSize => Math.Clamp(cellSize * 0.46, 12, 26)),
|
||||||
new DesktopComponentRuntimeRegistration(
|
new DesktopComponentRuntimeRegistration(
|
||||||
BuiltInComponentIds.HolidayCalendar,
|
BuiltInComponentIds.HolidayCalendar,
|
||||||
"component.holiday_calendar",
|
"component.holiday_calendar",
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone
|
|||||||
{
|
{
|
||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 14, 48));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 14, 48);
|
||||||
RootBorder.Padding = new Thickness(Math.Clamp(12 * scale, 6, 18));
|
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 6, 18));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetRecommendationInfoService(IRecommendationInfoService recommendationInfoService)
|
public void SetRecommendationInfoService(IRecommendationInfoService recommendationInfoService)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
@@ -18,7 +19,7 @@ using LanMountainDesktop.Theme;
|
|||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware
|
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentChromeContextAware
|
||||||
{
|
{
|
||||||
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
|
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
|
||||||
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
|
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
|
||||||
@@ -34,6 +35,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
private TimeZoneService? _timeZoneService;
|
private TimeZoneService? _timeZoneService;
|
||||||
private CancellationTokenSource? _refreshCts;
|
private CancellationTokenSource? _refreshCts;
|
||||||
private double _currentCellSize = 48;
|
private double _currentCellSize = 48;
|
||||||
|
private ComponentChromeContext? _chromeContext;
|
||||||
private double _phase;
|
private double _phase;
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
@@ -122,21 +124,34 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Extended4x4);
|
var metrics = HyperOS3WeatherTheme.ResolveMetrics(HyperOS3WeatherWidgetKind.Extended4x4);
|
||||||
var width = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 4;
|
var width = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 4;
|
||||||
var height = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 4;
|
var height = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 4;
|
||||||
var radius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 28, 54);
|
var radius = ComponentChromeCornerRadiusHelper.Scale(
|
||||||
RootBorder.CornerRadius = new CornerRadius(radius);
|
_currentCellSize * metrics.CornerRadiusScale,
|
||||||
BackgroundImageLayer.CornerRadius = new CornerRadius(radius);
|
28,
|
||||||
BackgroundMotionLayer.CornerRadius = new CornerRadius(radius);
|
54,
|
||||||
BackgroundTintLayer.CornerRadius = new CornerRadius(radius);
|
_chromeContext);
|
||||||
BackgroundLightLayer.CornerRadius = new CornerRadius(radius);
|
ComponentChromeCornerRadiusHelper.Apply(
|
||||||
BackgroundShadeLayer.CornerRadius = new CornerRadius(radius);
|
radius,
|
||||||
|
RootBorder,
|
||||||
|
BackgroundImageLayer,
|
||||||
|
BackgroundMotionLayer,
|
||||||
|
BackgroundTintLayer,
|
||||||
|
BackgroundLightLayer,
|
||||||
|
BackgroundShadeLayer);
|
||||||
var horizontalPadding = Math.Clamp(Math.Min(width * metrics.HorizontalPaddingScale * 0.30, width * 0.11), 4, 34);
|
var horizontalPadding = Math.Clamp(Math.Min(width * metrics.HorizontalPaddingScale * 0.30, width * 0.11), 4, 34);
|
||||||
var verticalPadding = Math.Clamp(Math.Min(height * metrics.VerticalPaddingScale * 0.30, height * 0.11), 4, 34);
|
var verticalPadding = Math.Clamp(Math.Min(height * metrics.VerticalPaddingScale * 0.30, height * 0.11), 4, 34);
|
||||||
ContentPaddingBorder.Padding = new Thickness(
|
ContentPaddingBorder.Padding = new Thickness(
|
||||||
horizontalPadding,
|
ComponentChromeCornerRadiusHelper.SafeValue(horizontalPadding, 4, 34, _chromeContext),
|
||||||
verticalPadding);
|
ComponentChromeCornerRadiusHelper.SafeValue(verticalPadding, 4, 34, _chromeContext));
|
||||||
ApplyTypography(width, height);
|
ApplyTypography(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetComponentChromeContext(ComponentChromeContext context)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
_chromeContext = context;
|
||||||
|
ApplyCellSize(_currentCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||||
{
|
{
|
||||||
if (_timeZoneService is not null)
|
if (_timeZoneService is not null)
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge
|
|||||||
var titleNeedsTwoLines = isUltraCompact || titleUnits >= (isCompact ? 13 : 17);
|
var titleNeedsTwoLines = isUltraCompact || titleUnits >= (isCompact ? 13 : 17);
|
||||||
var dateNeedsTwoLines = isUltraCompact || dateUnits >= (isCompact ? 15 : 20);
|
var dateNeedsTwoLines = isUltraCompact || dateUnits >= (isCompact ? 15 : 20);
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(shortSide * 0.13, 10, 46));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(shortSide * 0.13, 10, 46);
|
||||||
var padding = Math.Clamp(shortSide * 0.05, 4.5, 21);
|
var padding = ComponentChromeCornerRadiusHelper.SafeValue(shortSide * 0.05, 4.5, 21);
|
||||||
RootBorder.Padding = new Thickness(padding);
|
RootBorder.Padding = new Thickness(padding);
|
||||||
LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.028, 2.2, 12);
|
LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.028, 2.2, 12);
|
||||||
var rowWeights = ApplyAdaptiveRowHeights(isCompact, isUltraCompact, titleNeedsTwoLines, dateNeedsTwoLines);
|
var rowWeights = ApplyAdaptiveRowHeights(isCompact, isUltraCompact, titleNeedsTwoLines, dateNeedsTwoLines);
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ using Avalonia.Media.Imaging;
|
|||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Theme;
|
using LanMountainDesktop.Theme;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware
|
public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentChromeContextAware
|
||||||
{
|
{
|
||||||
private enum WeatherVisualKind
|
private enum WeatherVisualKind
|
||||||
{
|
{
|
||||||
@@ -117,6 +118,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private WeatherSnapshot? _latestSnapshot;
|
private WeatherSnapshot? _latestSnapshot;
|
||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
private double _currentCellSize = 48;
|
private double _currentCellSize = 48;
|
||||||
|
private ComponentChromeContext? _chromeContext;
|
||||||
private WeatherVisualKind _activeVisualKind = WeatherVisualKind.ClearDay;
|
private WeatherVisualKind _activeVisualKind = WeatherVisualKind.ClearDay;
|
||||||
private double _animationPhase;
|
private double _animationPhase;
|
||||||
private int _activeParticleCount;
|
private int _activeParticleCount;
|
||||||
@@ -254,6 +256,13 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetComponentChromeContext(ComponentChromeContext context)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
_chromeContext = context;
|
||||||
|
ApplyCellSize(_currentCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
public void ApplyCellSize(double cellSize)
|
public void ApplyCellSize(double cellSize)
|
||||||
{
|
{
|
||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
@@ -261,17 +270,23 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
|
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
|
||||||
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
|
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
|
||||||
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 46);
|
var cornerRadius = ComponentChromeCornerRadiusHelper.Scale(
|
||||||
|
_currentCellSize * metrics.CornerRadiusScale,
|
||||||
|
24,
|
||||||
|
46,
|
||||||
|
_chromeContext);
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
ComponentChromeCornerRadiusHelper.Apply(
|
||||||
BackgroundImageLayer.CornerRadius = new CornerRadius(cornerRadius);
|
cornerRadius,
|
||||||
BackgroundMotionLayer.CornerRadius = new CornerRadius(cornerRadius);
|
RootBorder,
|
||||||
BackgroundTintLayer.CornerRadius = new CornerRadius(cornerRadius);
|
BackgroundImageLayer,
|
||||||
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
|
BackgroundMotionLayer,
|
||||||
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
|
BackgroundTintLayer,
|
||||||
|
BackgroundLightLayer,
|
||||||
|
BackgroundShadeLayer);
|
||||||
ContentPaddingBorder.Padding = new Thickness(
|
ContentPaddingBorder.Padding = new Thickness(
|
||||||
Math.Clamp(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.034), 4, 22),
|
ComponentChromeCornerRadiusHelper.SafeValue(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.034), 4, 22, _chromeContext),
|
||||||
Math.Clamp(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.068), 3, 18));
|
ComponentChromeCornerRadiusHelper.SafeValue(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.068), 3, 18, _chromeContext));
|
||||||
ApplyAdaptiveTypography();
|
ApplyAdaptiveTypography();
|
||||||
ResetParticles();
|
ResetParticles();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,8 +400,8 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(32 * softScale, 16, 46));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46);
|
||||||
CardBorder.CornerRadius = new CornerRadius(Math.Clamp(32 * softScale, 16, 46));
|
CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46);
|
||||||
|
|
||||||
var horizontalPadding = Math.Clamp(14 * softScale, 8, 20);
|
var horizontalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||||
var verticalPadding = Math.Clamp(14 * softScale, 8, 20);
|
var verticalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||||
@@ -452,7 +452,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
|
|
||||||
visual.ImageHost.Width = imageWidth;
|
visual.ImageHost.Width = imageWidth;
|
||||||
visual.ImageHost.Height = imageHeight;
|
visual.ImageHost.Height = imageHeight;
|
||||||
visual.ImageHost.CornerRadius = new CornerRadius(Math.Clamp(imageHeight * 0.15, 8, 16));
|
visual.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(imageHeight * 0.15, 8, 16);
|
||||||
|
|
||||||
visual.TitleTextBlock.MaxWidth = textWidth;
|
visual.TitleTextBlock.MaxWidth = textWidth;
|
||||||
visual.TitleTextBlock.FontSize = titleFont;
|
visual.TitleTextBlock.FontSize = titleFont;
|
||||||
|
|||||||
@@ -182,8 +182,8 @@ public partial class LunarCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
{
|
{
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(30 * scale, 16, 44));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
|
||||||
RootBorder.Padding = new Thickness(Math.Clamp(16 * scale, 8, 24));
|
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 8, 24));
|
||||||
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 18);
|
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 18);
|
||||||
DividerBorder.Margin = new Thickness(
|
DividerBorder.Margin = new Thickness(
|
||||||
Math.Clamp(8 * scale, 3, 14),
|
Math.Clamp(8 * scale, 3, 14),
|
||||||
|
|||||||
@@ -217,8 +217,8 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget,
|
|||||||
{
|
{
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(28 * scale, 14, 40));
|
RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 14, 40);
|
||||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 22));
|
RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 22));
|
||||||
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 16);
|
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 16);
|
||||||
LayoutRoot.Width = Math.Clamp(280 * scale, 220, 420);
|
LayoutRoot.Width = Math.Clamp(280 * scale, 220, 420);
|
||||||
LayoutRoot.Height = Math.Clamp(280 * scale, 220, 420);
|
LayoutRoot.Height = Math.Clamp(280 * scale, 220, 420);
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Theme;
|
using LanMountainDesktop.Theme;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware
|
public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentChromeContextAware
|
||||||
{
|
{
|
||||||
private enum WeatherVisualKind
|
private enum WeatherVisualKind
|
||||||
{
|
{
|
||||||
@@ -115,6 +116,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
private WeatherSnapshot? _latestSnapshot;
|
private WeatherSnapshot? _latestSnapshot;
|
||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
private double _currentCellSize = 48;
|
private double _currentCellSize = 48;
|
||||||
|
private ComponentChromeContext? _chromeContext;
|
||||||
private WeatherVisualKind _activeVisualKind = WeatherVisualKind.ClearDay;
|
private WeatherVisualKind _activeVisualKind = WeatherVisualKind.ClearDay;
|
||||||
private double _animationPhase;
|
private double _animationPhase;
|
||||||
private int _activeParticleCount;
|
private int _activeParticleCount;
|
||||||
@@ -252,6 +254,13 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetComponentChromeContext(ComponentChromeContext context)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
_chromeContext = context;
|
||||||
|
ApplyCellSize(_currentCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
public void ApplyCellSize(double cellSize)
|
public void ApplyCellSize(double cellSize)
|
||||||
{
|
{
|
||||||
_currentCellSize = Math.Max(1, cellSize);
|
_currentCellSize = Math.Max(1, cellSize);
|
||||||
@@ -259,17 +268,23 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
|||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
|
var hostWidth = Bounds.Width > 1 ? Bounds.Width : Math.Max(140, _currentCellSize * 4);
|
||||||
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
|
var hostHeight = Bounds.Height > 1 ? Bounds.Height : Math.Max(78, _currentCellSize * 2);
|
||||||
var cornerRadius = Math.Clamp(_currentCellSize * metrics.CornerRadiusScale, 24, 46);
|
var cornerRadius = ComponentChromeCornerRadiusHelper.Scale(
|
||||||
|
_currentCellSize * metrics.CornerRadiusScale,
|
||||||
|
24,
|
||||||
|
46,
|
||||||
|
_chromeContext);
|
||||||
|
|
||||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
ComponentChromeCornerRadiusHelper.Apply(
|
||||||
BackgroundImageLayer.CornerRadius = new CornerRadius(cornerRadius);
|
cornerRadius,
|
||||||
BackgroundMotionLayer.CornerRadius = new CornerRadius(cornerRadius);
|
RootBorder,
|
||||||
BackgroundTintLayer.CornerRadius = new CornerRadius(cornerRadius);
|
BackgroundImageLayer,
|
||||||
BackgroundLightLayer.CornerRadius = new CornerRadius(cornerRadius);
|
BackgroundMotionLayer,
|
||||||
BackgroundShadeLayer.CornerRadius = new CornerRadius(cornerRadius);
|
BackgroundTintLayer,
|
||||||
|
BackgroundLightLayer,
|
||||||
|
BackgroundShadeLayer);
|
||||||
ContentPaddingBorder.Padding = new Thickness(
|
ContentPaddingBorder.Padding = new Thickness(
|
||||||
Math.Clamp(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.034), 4, 22),
|
ComponentChromeCornerRadiusHelper.SafeValue(Math.Min((_currentCellSize * metrics.HorizontalPaddingScale) * scale, hostWidth * 0.034), 4, 22, _chromeContext),
|
||||||
Math.Clamp(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.068), 3, 18));
|
ComponentChromeCornerRadiusHelper.SafeValue(Math.Min((_currentCellSize * metrics.VerticalPaddingScale) * scale, hostHeight * 0.068), 3, 18, _chromeContext));
|
||||||
ApplyAdaptiveTypography();
|
ApplyAdaptiveTypography();
|
||||||
ResetParticles();
|
ResetParticles();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<Border x:Name="RootBorder"
|
<Border x:Name="RootBorder"
|
||||||
CornerRadius="34"
|
CornerRadius="34"
|
||||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
Background="#2D5A8E"
|
||||||
ClipToBounds="True"
|
ClipToBounds="True"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
Padding="0">
|
Padding="0">
|
||||||
@@ -23,15 +23,15 @@
|
|||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Margin="0,-40,-40,0"
|
Margin="0,-40,-40,0"
|
||||||
CornerRadius="70"
|
CornerRadius="70"
|
||||||
Background="{DynamicResource SystemAccentColorLight2Brush}"
|
Background="#4A90D9"
|
||||||
Opacity="0.2"
|
Opacity="0.3"
|
||||||
IsHitTestVisible="False" />
|
IsHitTestVisible="False" />
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
|
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||||
<TextBlock x:Name="HeaderTextBlock"
|
<TextBlock x:Name="HeaderTextBlock"
|
||||||
Text="最近文档"
|
Text="最近文档"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
Foreground="#D8FFFFFF"
|
||||||
FontSize="18"
|
FontSize="18"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
PointerPressed="OnRefreshPointerPressed">
|
PointerPressed="OnRefreshPointerPressed">
|
||||||
<fi:SymbolIcon Symbol="ArrowSync"
|
<fi:SymbolIcon Symbol="ArrowSync"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
Foreground="#B8FFFFFF" />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -68,14 +68,14 @@
|
|||||||
Width="130"
|
Width="130"
|
||||||
Height="90"
|
Height="90"
|
||||||
CornerRadius="10"
|
CornerRadius="10"
|
||||||
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
|
Background="#3AFFFFFF"
|
||||||
Padding="10"
|
Padding="10"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
PointerPressed="OnDocumentCardPointerPressed">
|
PointerPressed="OnDocumentCardPointerPressed">
|
||||||
<Grid RowDefinitions="Auto,*,Auto">
|
<Grid RowDefinitions="Auto,*,Auto">
|
||||||
<TextBlock Grid.Row="0"
|
<TextBlock Grid.Row="0"
|
||||||
Text="{Binding FileName}"
|
Text="{Binding FileName}"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
Foreground="#D8FFFFFF"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
FontWeight="Medium"
|
FontWeight="Medium"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
VerticalAlignment="Top" />
|
VerticalAlignment="Top" />
|
||||||
<TextBlock Grid.Row="2"
|
<TextBlock Grid.Row="2"
|
||||||
Text="{Binding TimeAgo}"
|
Text="{Binding TimeAgo}"
|
||||||
Foreground="{DynamicResource AdaptiveTextTertiaryBrush}"
|
Foreground="#9AFFFFFF"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
MaxLines="1" />
|
MaxLines="1" />
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<TextBlock x:Name="StatusTextBlock"
|
<TextBlock x:Name="StatusTextBlock"
|
||||||
IsVisible="False"
|
IsVisible="False"
|
||||||
Text="暂无最近文档"
|
Text="暂无最近文档"
|
||||||
Foreground="{DynamicResource AdaptiveTextTertiaryBrush}"
|
Foreground="#9AFFFFFF"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Views.Components;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.Components;
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IComponentPlacementContextAware
|
||||||
{
|
{
|
||||||
private readonly IOfficeRecentDocumentsService _recentDocumentsService;
|
private readonly IOfficeRecentDocumentsService _recentDocumentsService;
|
||||||
|
private readonly IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||||
private List<OfficeRecentDocument> _documents = new();
|
private List<OfficeRecentDocument> _documents = new();
|
||||||
|
private string _componentId = BuiltInComponentIds.DesktopOfficeRecentDocuments;
|
||||||
|
private string _placementId = string.Empty;
|
||||||
|
private IReadOnlyList<string> _enabledSources = OfficeRecentDocumentSourceTypes.DefaultValues;
|
||||||
private bool _isOnActivePage;
|
private bool _isOnActivePage;
|
||||||
private bool _isEditMode;
|
private bool _isEditMode;
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
@@ -20,6 +26,7 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_recentDocumentsService = new OfficeRecentDocumentsService();
|
_recentDocumentsService = new OfficeRecentDocumentsService();
|
||||||
|
ReloadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyCellSize(double cellSize)
|
public void ApplyCellSize(double cellSize)
|
||||||
@@ -44,27 +51,45 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadDocuments()
|
public void SetComponentPlacementContext(string componentId, string? placementId)
|
||||||
{
|
{
|
||||||
|
_componentId = string.IsNullOrWhiteSpace(componentId)
|
||||||
|
? BuiltInComponentIds.DesktopOfficeRecentDocuments
|
||||||
|
: componentId.Trim();
|
||||||
|
_placementId = placementId?.Trim() ?? string.Empty;
|
||||||
|
ReloadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void LoadDocuments()
|
||||||
|
{
|
||||||
|
if (_isLoading)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
ReloadSettings();
|
||||||
StatusTextBlock.IsVisible = false;
|
StatusTextBlock.IsVisible = false;
|
||||||
|
DocumentsItemsControl.ItemsSource = null;
|
||||||
|
|
||||||
_documents = _recentDocumentsService.GetRecentDocuments(20);
|
var enabledSources = _enabledSources.ToArray();
|
||||||
|
_documents = await Task.Run(() => _recentDocumentsService.GetRecentDocuments(20, enabledSources));
|
||||||
|
|
||||||
if (_documents.Count == 0)
|
if (_documents.Count == 0)
|
||||||
{
|
{
|
||||||
StatusTextBlock.Text = "暂无最近文档";
|
StatusTextBlock.Text = "\u6682\u65e0\u6700\u8fd1\u6587\u6863";
|
||||||
StatusTextBlock.IsVisible = true;
|
StatusTextBlock.IsVisible = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDisplay();
|
UpdateDisplay();
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatusTextBlock.Text = "加载失败";
|
AppLogger.Warn("OfficeRecentDocsWidget", "Failed to load recent Office documents.", ex);
|
||||||
|
StatusTextBlock.Text = "\u52a0\u8f7d\u5931\u8d25";
|
||||||
StatusTextBlock.IsVisible = true;
|
StatusTextBlock.IsVisible = true;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -73,6 +98,14 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReloadSettings()
|
||||||
|
{
|
||||||
|
var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId);
|
||||||
|
_enabledSources = OfficeRecentDocumentSourceTypes.NormalizeValues(
|
||||||
|
snapshot.OfficeRecentDocumentsEnabledSources,
|
||||||
|
useDefaultWhenEmpty: snapshot.OfficeRecentDocumentsEnabledSources is null);
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateDisplay()
|
private void UpdateDisplay()
|
||||||
{
|
{
|
||||||
var displayItems = _documents.Select(d => new OfficeRecentDocumentViewModel
|
var displayItems = _documents.Select(d => new OfficeRecentDocumentViewModel
|
||||||
@@ -90,15 +123,29 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen
|
|||||||
var span = DateTime.Now - dateTime;
|
var span = DateTime.Now - dateTime;
|
||||||
|
|
||||||
if (span.TotalMinutes < 1)
|
if (span.TotalMinutes < 1)
|
||||||
return "刚刚";
|
{
|
||||||
|
return "\u521a\u521a";
|
||||||
|
}
|
||||||
|
|
||||||
if (span.TotalMinutes < 60)
|
if (span.TotalMinutes < 60)
|
||||||
return $"{(int)span.TotalMinutes} 分钟前";
|
{
|
||||||
|
return $"{(int)span.TotalMinutes} \u5206\u949f\u524d";
|
||||||
|
}
|
||||||
|
|
||||||
if (span.TotalHours < 24)
|
if (span.TotalHours < 24)
|
||||||
return $"{(int)span.TotalHours} 小时前";
|
{
|
||||||
|
return $"{(int)span.TotalHours} \u5c0f\u65f6\u524d";
|
||||||
|
}
|
||||||
|
|
||||||
if (span.TotalDays < 7)
|
if (span.TotalDays < 7)
|
||||||
return $"{(int)span.TotalDays} 天前";
|
{
|
||||||
|
return $"{(int)span.TotalDays} \u5929\u524d";
|
||||||
|
}
|
||||||
|
|
||||||
if (span.TotalDays < 30)
|
if (span.TotalDays < 30)
|
||||||
return $"{(int)(span.TotalDays / 7)} 周前";
|
{
|
||||||
|
return $"{(int)(span.TotalDays / 7)} \u5468\u524d";
|
||||||
|
}
|
||||||
|
|
||||||
return dateTime.ToString("MM/dd");
|
return dateTime.ToString("MM/dd");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,15 +63,15 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
|||||||
var chromeScale = Math.Clamp(rawScale, 0.62, 2.0);
|
var chromeScale = Math.Clamp(rawScale, 0.62, 2.0);
|
||||||
var contentScale = Math.Clamp(rawScale, 0.74, 1.0);
|
var contentScale = Math.Clamp(rawScale, 0.74, 1.0);
|
||||||
|
|
||||||
var rootRadius = Math.Clamp(34 * chromeScale, 16, 56);
|
var rootRadius = ComponentChromeCornerRadiusHelper.Scale(34 * chromeScale, 16, 56);
|
||||||
RootBorder.CornerRadius = new CornerRadius(rootRadius);
|
RootBorder.CornerRadius = rootRadius;
|
||||||
RootBorder.Padding = new Thickness(0);
|
RootBorder.Padding = new Thickness(0);
|
||||||
RecorderCardBorder.CornerRadius = new CornerRadius(rootRadius);
|
RecorderCardBorder.CornerRadius = rootRadius;
|
||||||
RecorderContentGrid.Margin = new Thickness(
|
RecorderContentGrid.Margin = new Thickness(
|
||||||
Math.Clamp(24 * contentScale, 14, 26),
|
ComponentChromeCornerRadiusHelper.SafeValue(24 * contentScale, 14, 26),
|
||||||
Math.Clamp(18 * contentScale, 10, 22),
|
ComponentChromeCornerRadiusHelper.SafeValue(18 * contentScale, 10, 22),
|
||||||
Math.Clamp(24 * contentScale, 14, 26),
|
ComponentChromeCornerRadiusHelper.SafeValue(24 * contentScale, 14, 26),
|
||||||
Math.Clamp(18 * contentScale, 10, 24));
|
ComponentChromeCornerRadiusHelper.SafeValue(18 * contentScale, 10, 24));
|
||||||
|
|
||||||
var sideButtonSize = Math.Clamp(54 * contentScale, 34, 58);
|
var sideButtonSize = Math.Clamp(54 * contentScale, 34, 58);
|
||||||
DiscardButtonBorder.Width = sideButtonSize;
|
DiscardButtonBorder.Width = sideButtonSize;
|
||||||
|
|||||||
119
LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml
Normal file
119
LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:fi="using:FluentIcons.Avalonia"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="280"
|
||||||
|
d:DesignHeight="280"
|
||||||
|
x:Class="LanMountainDesktop.Views.Components.RemovableStorageWidget">
|
||||||
|
|
||||||
|
<Border x:Name="RootBorder"
|
||||||
|
CornerRadius="28"
|
||||||
|
BorderThickness="1"
|
||||||
|
Padding="16"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="AccentOrb"
|
||||||
|
Width="132"
|
||||||
|
Height="132"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="0,-48,-48,0"
|
||||||
|
CornerRadius="66"
|
||||||
|
IsHitTestVisible="False" />
|
||||||
|
|
||||||
|
<Border x:Name="AccentGlow"
|
||||||
|
Height="76"
|
||||||
|
Margin="-18,0,-18,-34"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
CornerRadius="38"
|
||||||
|
Opacity="0.42"
|
||||||
|
IsHitTestVisible="False" />
|
||||||
|
|
||||||
|
<Grid x:Name="LayoutGrid"
|
||||||
|
RowDefinitions="Auto,*,Auto,Auto"
|
||||||
|
RowSpacing="10">
|
||||||
|
<Grid x:Name="HeaderGrid"
|
||||||
|
ColumnDefinitions="Auto,*"
|
||||||
|
ColumnSpacing="12">
|
||||||
|
<Border x:Name="IconBadge"
|
||||||
|
Width="44"
|
||||||
|
Height="44"
|
||||||
|
CornerRadius="22"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<fi:FluentIcon x:Name="DriveIcon"
|
||||||
|
Icon="UsbStick"
|
||||||
|
IconVariant="Regular"
|
||||||
|
FontSize="24"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel x:Name="HeaderTextStack"
|
||||||
|
Grid.Column="1"
|
||||||
|
Spacing="2"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="DriveNameTextBlock"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock x:Name="DriveDetailTextBlock"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock x:Name="StatusTextBlock"
|
||||||
|
Grid.Row="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<Button x:Name="OpenButton"
|
||||||
|
Grid.Row="2"
|
||||||
|
Height="42"
|
||||||
|
Padding="14,0"
|
||||||
|
CornerRadius="999"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
Click="OnOpenClick">
|
||||||
|
<Grid ColumnDefinitions="Auto,*"
|
||||||
|
ColumnSpacing="8">
|
||||||
|
<fi:FluentIcon x:Name="OpenButtonIcon"
|
||||||
|
Icon="OpenFolder"
|
||||||
|
IconVariant="Regular"
|
||||||
|
FontSize="16"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="OpenButtonTextBlock"
|
||||||
|
Grid.Column="1"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button x:Name="EjectButton"
|
||||||
|
Grid.Row="3"
|
||||||
|
Height="42"
|
||||||
|
Padding="14,0"
|
||||||
|
CornerRadius="999"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
Click="OnEjectClick">
|
||||||
|
<Grid ColumnDefinitions="Auto,*"
|
||||||
|
ColumnSpacing="8">
|
||||||
|
<fi:FluentIcon x:Name="EjectButtonIcon"
|
||||||
|
Icon="ArrowEject"
|
||||||
|
IconVariant="Regular"
|
||||||
|
FontSize="16"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="EjectButtonTextBlock"
|
||||||
|
Grid.Column="1"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user