This commit is contained in:
lincube
2026-03-05 14:34:33 +08:00
parent d182925b58
commit f3e7f88a39
15 changed files with 205 additions and 23 deletions

View File

@@ -181,8 +181,8 @@ jobs:
$compileArgs = @( $compileArgs = @(
"/DMyAppVersion=$version", "/DMyAppVersion=$version",
"/DPublishDir=`"$publishDir`"", "/DPublishDir=$publishDir",
"/DMyOutputDir=`"$outputDir`"", "/DMyOutputDir=$outputDir",
"/DMyAppArch=$arch", "/DMyAppArch=$arch",
$installerScript $installerScript
) )

View File

@@ -19,6 +19,7 @@
<Application.Styles> <Application.Styles>
<sty:FluentAvaloniaTheme /> <sty:FluentAvaloniaTheme />
<mi:MaterialIconStyles /> <mi:MaterialIconStyles />
<StyleInclude Source="avares://LanMountainDesktop/Styles/MotionTokens.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/GlassModule.axaml" /> <StyleInclude Source="avares://LanMountainDesktop/Styles/GlassModule.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsAnimations.axaml" /> <StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsAnimations.axaml" />

View File

@@ -3,6 +3,7 @@ using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading; using Avalonia.Threading;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Behaviors; namespace LanMountainDesktop.Behaviors;
@@ -109,7 +110,7 @@ public class PanelIntroAnimationBehavior
var index = 0; var index = 0;
var timer = new DispatcherTimer(DispatcherPriority.Background) var timer = new DispatcherTimer(DispatcherPriority.Background)
{ {
Interval = TimeSpan.FromMilliseconds(24) Interval = UiMotionTokens.StaggerStepInterval
}; };
timer.Tick += (_, _) => timer.Tick += (_, _) =>
{ {

View File

@@ -4,11 +4,14 @@ using Avalonia.Animation.Easings;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Behaviors; namespace LanMountainDesktop.Behaviors;
public class PopupIntroAnimationBehavior public class PopupIntroAnimationBehavior
{ {
private static readonly Easing StandardEasing = Easing.Parse(UiMotionTokens.StandardBezier);
public static readonly AttachedProperty<bool> IsEnabledProperty = public static readonly AttachedProperty<bool> IsEnabledProperty =
AvaloniaProperty.RegisterAttached<PopupIntroAnimationBehavior, Control, bool>("IsEnabled"); AvaloniaProperty.RegisterAttached<PopupIntroAnimationBehavior, Control, bool>("IsEnabled");
@@ -94,16 +97,16 @@ public class PopupIntroAnimationBehavior
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation(); var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
opacityAnimation.Target = nameof(compositionVisual.Opacity); opacityAnimation.Target = nameof(compositionVisual.Opacity);
opacityAnimation.Duration = TimeSpan.FromMilliseconds(160); opacityAnimation.Duration = UiMotionTokens.Standard;
opacityAnimation.InsertKeyFrame(0f, 0f); opacityAnimation.InsertKeyFrame(0f, 0f);
opacityAnimation.InsertKeyFrame(1f, 1f, Easing.Parse("0.22, 1, 0.36, 1")); opacityAnimation.InsertKeyFrame(1f, 1f, StandardEasing);
compositionVisual.StartAnimation(nameof(compositionVisual.Opacity), opacityAnimation); compositionVisual.StartAnimation(nameof(compositionVisual.Opacity), opacityAnimation);
var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation(); var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation();
scaleAnimation.Target = nameof(compositionVisual.Scale); scaleAnimation.Target = nameof(compositionVisual.Scale);
scaleAnimation.Duration = TimeSpan.FromMilliseconds(160); scaleAnimation.Duration = UiMotionTokens.Standard;
scaleAnimation.InsertKeyFrame(0f, compositionVisual.Scale with { X = 0.94, Y = 0.94 }); scaleAnimation.InsertKeyFrame(0f, compositionVisual.Scale with { X = 0.94, Y = 0.94 });
scaleAnimation.InsertKeyFrame(1f, compositionVisual.Scale with { X = 1, Y = 1 }, Easing.Parse("0.22, 1, 0.36, 1")); scaleAnimation.InsertKeyFrame(1f, compositionVisual.Scale with { X = 1, Y = 1 }, StandardEasing);
compositionVisual.StartAnimation(nameof(compositionVisual.Scale), scaleAnimation); compositionVisual.StartAnimation(nameof(compositionVisual.Scale), scaleAnimation);
} }

View File

@@ -80,4 +80,61 @@ public sealed class AppSettingsSnapshot
public bool StudyEnvironmentShowDbfs { get; set; } public bool StudyEnvironmentShowDbfs { get; set; }
public AppSettingsSnapshot Clone()
{
var clone = (AppSettingsSnapshot)MemberwiseClone();
clone.TopStatusComponentIds = TopStatusComponentIds is { Count: > 0 }
? new List<string>(TopStatusComponentIds)
: [];
clone.PinnedTaskbarActions = PinnedTaskbarActions is { Count: > 0 }
? new List<string>(PinnedTaskbarActions)
: [];
var placements = new List<DesktopComponentPlacementSnapshot>(DesktopComponentPlacements?.Count ?? 0);
if (DesktopComponentPlacements is not null)
{
foreach (var placement in DesktopComponentPlacements)
{
if (placement is null)
{
continue;
}
placements.Add(new DesktopComponentPlacementSnapshot
{
PlacementId = placement.PlacementId,
PageIndex = placement.PageIndex,
ComponentId = placement.ComponentId,
Row = placement.Row,
Column = placement.Column,
WidthCells = placement.WidthCells,
HeightCells = placement.HeightCells
});
}
}
clone.DesktopComponentPlacements = placements;
var schedules = new List<ImportedClassScheduleSnapshot>(ImportedClassSchedules?.Count ?? 0);
if (ImportedClassSchedules is not null)
{
foreach (var schedule in ImportedClassSchedules)
{
if (schedule is null)
{
continue;
}
schedules.Add(new ImportedClassScheduleSnapshot
{
Id = schedule.Id,
DisplayName = schedule.DisplayName,
FilePath = schedule.FilePath
});
}
}
clone.ImportedClassSchedules = schedules;
return clone;
}
} }

View File

@@ -11,6 +11,13 @@ public sealed class AppSettingsService
{ {
WriteIndented = true WriteIndented = true
}; };
private static readonly object CacheGate = new();
private static readonly TimeSpan CacheProbeInterval = TimeSpan.FromMilliseconds(400);
private static string? _cachedPath;
private static AppSettingsSnapshot? _cachedSnapshot;
private static DateTime _cachedWriteTimeUtc = DateTime.MinValue;
private static DateTime _lastProbeUtc = DateTime.MinValue;
private readonly string _settingsPath; private readonly string _settingsPath;
@@ -25,14 +32,32 @@ public sealed class AppSettingsService
{ {
try try
{ {
if (!File.Exists(_settingsPath)) lock (CacheGate)
{ {
return new AppSettingsSnapshot(); var nowUtc = DateTime.UtcNow;
} if (TryGetCachedWithoutProbe(nowUtc, out var cached))
{
return cached;
}
var json = File.ReadAllText(_settingsPath); var hasFile = File.Exists(_settingsPath);
var snapshot = JsonSerializer.Deserialize<AppSettingsSnapshot>(json, SerializerOptions); var writeTimeUtc = hasFile
return snapshot ?? new AppSettingsSnapshot(); ? File.GetLastWriteTimeUtc(_settingsPath)
: DateTime.MinValue;
_lastProbeUtc = nowUtc;
if (TryGetCachedAfterProbe(writeTimeUtc, out cached))
{
return cached;
}
var loadedSnapshot = hasFile
? LoadSnapshotFromDisk()
: new AppSettingsSnapshot();
UpdateCache(loadedSnapshot, writeTimeUtc, nowUtc);
return loadedSnapshot.Clone();
}
} }
catch catch
{ {
@@ -42,6 +67,8 @@ public sealed class AppSettingsService
public void Save(AppSettingsSnapshot snapshot) public void Save(AppSettingsSnapshot snapshot)
{ {
var snapshotToPersist = snapshot?.Clone() ?? new AppSettingsSnapshot();
try try
{ {
var directory = Path.GetDirectoryName(_settingsPath); var directory = Path.GetDirectoryName(_settingsPath);
@@ -50,13 +77,70 @@ public sealed class AppSettingsService
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
} }
var json = JsonSerializer.Serialize(snapshot, SerializerOptions); var json = JsonSerializer.Serialize(snapshotToPersist, SerializerOptions);
File.WriteAllText(_settingsPath, json); File.WriteAllText(_settingsPath, json);
var writeTimeUtc = File.Exists(_settingsPath)
? File.GetLastWriteTimeUtc(_settingsPath)
: DateTime.UtcNow;
lock (CacheGate)
{
UpdateCache(snapshotToPersist, writeTimeUtc, DateTime.UtcNow);
}
} }
catch catch
{ {
// Swallow persistence errors to keep UI interactions uninterrupted. // Swallow persistence errors to keep UI interactions uninterrupted.
} }
} }
}
private bool TryGetCachedWithoutProbe(DateTime nowUtc, out AppSettingsSnapshot snapshot)
{
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) &&
_cachedSnapshot is not null &&
nowUtc - _lastProbeUtc < CacheProbeInterval)
{
snapshot = _cachedSnapshot.Clone();
return true;
}
snapshot = null!;
return false;
}
private bool TryGetCachedAfterProbe(DateTime writeTimeUtc, out AppSettingsSnapshot snapshot)
{
if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) &&
_cachedSnapshot is not null &&
writeTimeUtc == _cachedWriteTimeUtc)
{
snapshot = _cachedSnapshot.Clone();
return true;
}
snapshot = null!;
return false;
}
private AppSettingsSnapshot LoadSnapshotFromDisk()
{
try
{
var json = File.ReadAllText(_settingsPath);
return JsonSerializer.Deserialize<AppSettingsSnapshot>(json, SerializerOptions) ?? new AppSettingsSnapshot();
}
catch
{
return new AppSettingsSnapshot();
}
}
private void UpdateCache(AppSettingsSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc)
{
_cachedPath = _settingsPath;
_cachedSnapshot = snapshot.Clone();
_cachedWriteTimeUtc = writeTimeUtc;
_lastProbeUtc = probeTimeUtc;
}
}

View File

@@ -0,0 +1,14 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<x:String x:Key="MotionEasingStandard">0.22,1,0.36,1</x:String>
<x:String x:Key="MotionDurationFast">0:0:0.12</x:String>
<x:String x:Key="MotionDurationStandard">0:0:0.16</x:String>
<x:String x:Key="MotionDurationSlow">0:0:0.20</x:String>
<x:String x:Key="MotionDurationPage">0:0:0.24</x:String>
<x:String x:Key="MotionDurationIntro">0:0:0.32</x:String>
<x:Double x:Key="MotionBackdropBlurRadiusStrong">30</x:Double>
</Styles.Resources>
</Styles>

View File

@@ -0,0 +1,17 @@
using System;
namespace LanMountainDesktop.Theme;
public static class UiMotionTokens
{
public static readonly TimeSpan Fast = TimeSpan.FromMilliseconds(120);
public static readonly TimeSpan Standard = TimeSpan.FromMilliseconds(160);
public static readonly TimeSpan Slow = TimeSpan.FromMilliseconds(200);
public static readonly TimeSpan Page = TimeSpan.FromMilliseconds(240);
public static readonly TimeSpan Intro = TimeSpan.FromMilliseconds(320);
public static readonly TimeSpan StaggerStepInterval = TimeSpan.FromMilliseconds(24);
public static readonly TimeSpan WeatherAnimationFrameInterval = TimeSpan.FromMilliseconds(64);
public const string StandardBezier = "0.22, 1, 0.36, 1";
}

View File

@@ -11,6 +11,7 @@ using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
@@ -19,7 +20,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromMinutes(12) }; private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromMinutes(12) };
private readonly DispatcherTimer _animationTimer = new() { Interval = TimeSpan.FromMilliseconds(48) }; private readonly DispatcherTimer _animationTimer = new() { Interval = UiMotionTokens.WeatherAnimationFrameInterval };
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1); private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
private readonly TranslateTransform _backgroundMotionTranslateTransform = new(); private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();

View File

@@ -13,6 +13,7 @@ using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
@@ -89,7 +90,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private readonly DispatcherTimer _backgroundAnimationTimer = new() private readonly DispatcherTimer _backgroundAnimationTimer = new()
{ {
Interval = TimeSpan.FromMilliseconds(48) Interval = UiMotionTokens.WeatherAnimationFrameInterval
}; };
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();

View File

@@ -11,6 +11,7 @@ using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
@@ -87,7 +88,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private readonly DispatcherTimer _backgroundAnimationTimer = new() private readonly DispatcherTimer _backgroundAnimationTimer = new()
{ {
Interval = TimeSpan.FromMilliseconds(48) Interval = UiMotionTokens.WeatherAnimationFrameInterval
}; };
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();

View File

@@ -86,7 +86,7 @@
Opacity="0.62" Opacity="0.62"
Stretch="UniformToFill"> Stretch="UniformToFill">
<Image.Effect> <Image.Effect>
<BlurEffect Radius="42" /> <BlurEffect Radius="{DynamicResource MotionBackdropBlurRadiusStrong}" />
</Image.Effect> </Image.Effect>
</Image> </Image>
</Border> </Border>

View File

@@ -13,6 +13,7 @@ using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
@@ -83,7 +84,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private readonly DispatcherTimer _backgroundAnimationTimer = new() private readonly DispatcherTimer _backgroundAnimationTimer = new()
{ {
Interval = TimeSpan.FromMilliseconds(48) Interval = UiMotionTokens.WeatherAnimationFrameInterval
}; };
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();

View File

@@ -14,6 +14,7 @@ using FluentIcons.Avalonia;
using FluentIcons.Common; using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components; using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views; namespace LanMountainDesktop.Views;
@@ -408,7 +409,7 @@ public partial class MainWindow
{ {
OpenSettingsPage(); OpenSettingsPage();
} }
}, TimeSpan.FromMilliseconds(200)); }, UiMotionTokens.Slow);
} }
private void InitializeDesktopComponentDragHandlers() private void InitializeDesktopComponentDragHandlers()
@@ -872,7 +873,7 @@ public partial class MainWindow
{ {
ComponentSettingsContentHost.Content = null; ComponentSettingsContentHost.Content = null;
} }
}, TimeSpan.FromMilliseconds(200)); }, UiMotionTokens.Slow);
} }
private void AddDesktopPage() private void AddDesktopPage()

View File

@@ -58,7 +58,7 @@ public partial class MainWindow : Window
private const int MinEdgeInsetPercent = 0; private const int MinEdgeInsetPercent = 0;
private const int MaxEdgeInsetPercent = 30; private const int MaxEdgeInsetPercent = 30;
private const int DefaultEdgeInsetPercent = 18; private const int DefaultEdgeInsetPercent = 18;
private const int SettingsTransitionDurationMs = 240; private static readonly int SettingsTransitionDurationMs = (int)UiMotionTokens.Page.TotalMilliseconds;
private const double WallpaperPreviewMaxWidth = 520; private const double WallpaperPreviewMaxWidth = 520;
private const double LightBackgroundLuminanceThreshold = 0.57; private const double LightBackgroundLuminanceThreshold = 0.57;
private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle"; private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle";