diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0ba330..90a5352 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -181,8 +181,8 @@ jobs: $compileArgs = @( "/DMyAppVersion=$version", - "/DPublishDir=`"$publishDir`"", - "/DMyOutputDir=`"$outputDir`"", + "/DPublishDir=$publishDir", + "/DMyOutputDir=$outputDir", "/DMyAppArch=$arch", $installerScript ) diff --git a/LanMountainDesktop/App.axaml b/LanMountainDesktop/App.axaml index c098505..1ba1c7e 100644 --- a/LanMountainDesktop/App.axaml +++ b/LanMountainDesktop/App.axaml @@ -19,6 +19,7 @@ + diff --git a/LanMountainDesktop/Behaviors/PanelIntroAnimationBehavior.cs b/LanMountainDesktop/Behaviors/PanelIntroAnimationBehavior.cs index 279a831..3c6358b 100644 --- a/LanMountainDesktop/Behaviors/PanelIntroAnimationBehavior.cs +++ b/LanMountainDesktop/Behaviors/PanelIntroAnimationBehavior.cs @@ -3,6 +3,7 @@ using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Threading; +using LanMountainDesktop.Theme; namespace LanMountainDesktop.Behaviors; @@ -109,7 +110,7 @@ public class PanelIntroAnimationBehavior var index = 0; var timer = new DispatcherTimer(DispatcherPriority.Background) { - Interval = TimeSpan.FromMilliseconds(24) + Interval = UiMotionTokens.StaggerStepInterval }; timer.Tick += (_, _) => { diff --git a/LanMountainDesktop/Behaviors/PopupIntroAnimationBehavior.cs b/LanMountainDesktop/Behaviors/PopupIntroAnimationBehavior.cs index 343b08d..ddb5754 100644 --- a/LanMountainDesktop/Behaviors/PopupIntroAnimationBehavior.cs +++ b/LanMountainDesktop/Behaviors/PopupIntroAnimationBehavior.cs @@ -4,11 +4,14 @@ using Avalonia.Animation.Easings; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Rendering.Composition; +using LanMountainDesktop.Theme; namespace LanMountainDesktop.Behaviors; public class PopupIntroAnimationBehavior { + private static readonly Easing StandardEasing = Easing.Parse(UiMotionTokens.StandardBezier); + public static readonly AttachedProperty IsEnabledProperty = AvaloniaProperty.RegisterAttached("IsEnabled"); @@ -94,16 +97,16 @@ public class PopupIntroAnimationBehavior var opacityAnimation = compositor.CreateScalarKeyFrameAnimation(); opacityAnimation.Target = nameof(compositionVisual.Opacity); - opacityAnimation.Duration = TimeSpan.FromMilliseconds(160); + opacityAnimation.Duration = UiMotionTokens.Standard; 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); var scaleAnimation = compositor.CreateVector3DKeyFrameAnimation(); 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(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); } diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index d35b9b5..7e5418d 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -80,4 +80,61 @@ public sealed class AppSettingsSnapshot public bool StudyEnvironmentShowDbfs { get; set; } + public AppSettingsSnapshot Clone() + { + var clone = (AppSettingsSnapshot)MemberwiseClone(); + + clone.TopStatusComponentIds = TopStatusComponentIds is { Count: > 0 } + ? new List(TopStatusComponentIds) + : []; + clone.PinnedTaskbarActions = PinnedTaskbarActions is { Count: > 0 } + ? new List(PinnedTaskbarActions) + : []; + + var placements = new List(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(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; + } } diff --git a/LanMountainDesktop/Services/AppSettingsService.cs b/LanMountainDesktop/Services/AppSettingsService.cs index a22bb09..ff19aec 100644 --- a/LanMountainDesktop/Services/AppSettingsService.cs +++ b/LanMountainDesktop/Services/AppSettingsService.cs @@ -11,6 +11,13 @@ public sealed class AppSettingsService { 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; @@ -25,14 +32,32 @@ public sealed class AppSettingsService { 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 snapshot = JsonSerializer.Deserialize(json, SerializerOptions); - return snapshot ?? new AppSettingsSnapshot(); + var hasFile = File.Exists(_settingsPath); + var writeTimeUtc = hasFile + ? 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 { @@ -42,6 +67,8 @@ public sealed class AppSettingsService public void Save(AppSettingsSnapshot snapshot) { + var snapshotToPersist = snapshot?.Clone() ?? new AppSettingsSnapshot(); + try { var directory = Path.GetDirectoryName(_settingsPath); @@ -50,13 +77,70 @@ public sealed class AppSettingsService Directory.CreateDirectory(directory); } - var json = JsonSerializer.Serialize(snapshot, SerializerOptions); + var json = JsonSerializer.Serialize(snapshotToPersist, SerializerOptions); File.WriteAllText(_settingsPath, json); + + var writeTimeUtc = File.Exists(_settingsPath) + ? File.GetLastWriteTimeUtc(_settingsPath) + : DateTime.UtcNow; + + lock (CacheGate) + { + UpdateCache(snapshotToPersist, writeTimeUtc, DateTime.UtcNow); + } } catch { // 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(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; + } +} diff --git a/LanMountainDesktop/Styles/MotionTokens.axaml b/LanMountainDesktop/Styles/MotionTokens.axaml new file mode 100644 index 0000000..1de1963 --- /dev/null +++ b/LanMountainDesktop/Styles/MotionTokens.axaml @@ -0,0 +1,14 @@ + + + 0.22,1,0.36,1 + + 0:0:0.12 + 0:0:0.16 + 0:0:0.20 + 0:0:0.24 + 0:0:0.32 + + 30 + + diff --git a/LanMountainDesktop/Theme/UiMotionTokens.cs b/LanMountainDesktop/Theme/UiMotionTokens.cs new file mode 100644 index 0000000..c0c92f6 --- /dev/null +++ b/LanMountainDesktop/Theme/UiMotionTokens.cs @@ -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"; +} diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs index 2f794c5..75c1537 100644 --- a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Media; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; +using LanMountainDesktop.Theme; namespace LanMountainDesktop.Views.Components; @@ -19,7 +20,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); 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 TranslateTransform _backgroundMotionTranslateTransform = new(); private readonly AppSettingsService _settingsService = new(); diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs index bcc44a3..b21404b 100644 --- a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs @@ -13,6 +13,7 @@ using Avalonia.Platform; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; +using LanMountainDesktop.Theme; namespace LanMountainDesktop.Views.Components; @@ -89,7 +90,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, private readonly DispatcherTimer _backgroundAnimationTimer = new() { - Interval = TimeSpan.FromMilliseconds(48) + Interval = UiMotionTokens.WeatherAnimationFrameInterval }; private readonly AppSettingsService _settingsService = new(); diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs index da809c0..3c88f95 100644 --- a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Media; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; +using LanMountainDesktop.Theme; namespace LanMountainDesktop.Views.Components; @@ -87,7 +88,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge private readonly DispatcherTimer _backgroundAnimationTimer = new() { - Interval = TimeSpan.FromMilliseconds(48) + Interval = UiMotionTokens.WeatherAnimationFrameInterval }; private readonly AppSettingsService _settingsService = new(); diff --git a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml index 10acbb9..890f6a3 100644 --- a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml +++ b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml @@ -86,7 +86,7 @@ Opacity="0.62" Stretch="UniformToFill"> - + diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs index 2e66f05..9152fa2 100644 --- a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs @@ -13,6 +13,7 @@ using Avalonia.Platform; using Avalonia.Threading; using LanMountainDesktop.Models; using LanMountainDesktop.Services; +using LanMountainDesktop.Theme; namespace LanMountainDesktop.Views.Components; @@ -83,7 +84,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk private readonly DispatcherTimer _backgroundAnimationTimer = new() { - Interval = TimeSpan.FromMilliseconds(48) + Interval = UiMotionTokens.WeatherAnimationFrameInterval }; private readonly AppSettingsService _settingsService = new(); diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index 93fc29d..a6cff0b 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -14,6 +14,7 @@ using FluentIcons.Avalonia; using FluentIcons.Common; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; +using LanMountainDesktop.Theme; using LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views; @@ -408,7 +409,7 @@ public partial class MainWindow { OpenSettingsPage(); } - }, TimeSpan.FromMilliseconds(200)); + }, UiMotionTokens.Slow); } private void InitializeDesktopComponentDragHandlers() @@ -872,7 +873,7 @@ public partial class MainWindow { ComponentSettingsContentHost.Content = null; } - }, TimeSpan.FromMilliseconds(200)); + }, UiMotionTokens.Slow); } private void AddDesktopPage() diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs index 8d1957e..ccba2e0 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml.cs +++ b/LanMountainDesktop/Views/MainWindow.axaml.cs @@ -58,7 +58,7 @@ public partial class MainWindow : Window private const int MinEdgeInsetPercent = 0; private const int MaxEdgeInsetPercent = 30; 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 LightBackgroundLuminanceThreshold = 0.57; private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle";