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 = @(
"/DMyAppVersion=$version",
"/DPublishDir=`"$publishDir`"",
"/DMyOutputDir=`"$outputDir`"",
"/DPublishDir=$publishDir",
"/DMyOutputDir=$outputDir",
"/DMyAppArch=$arch",
$installerScript
)

View File

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

View File

@@ -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 += (_, _) =>
{

View File

@@ -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<bool> IsEnabledProperty =
AvaloniaProperty.RegisterAttached<PopupIntroAnimationBehavior, Control, bool>("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);
}

View File

@@ -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<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
};
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<AppSettingsSnapshot>(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<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 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();

View File

@@ -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();

View File

@@ -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();

View File

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

View File

@@ -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();

View File

@@ -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()

View File

@@ -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";