mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.3.14
This commit is contained in:
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -181,8 +181,8 @@ jobs:
|
||||
|
||||
$compileArgs = @(
|
||||
"/DMyAppVersion=$version",
|
||||
"/DPublishDir=`"$publishDir`"",
|
||||
"/DMyOutputDir=`"$outputDir`"",
|
||||
"/DPublishDir=$publishDir",
|
||||
"/DMyOutputDir=$outputDir",
|
||||
"/DMyAppArch=$arch",
|
||||
$installerScript
|
||||
)
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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 += (_, _) =>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
14
LanMountainDesktop/Styles/MotionTokens.axaml
Normal file
14
LanMountainDesktop/Styles/MotionTokens.axaml
Normal 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>
|
||||
17
LanMountainDesktop/Theme/UiMotionTokens.cs
Normal file
17
LanMountainDesktop/Theme/UiMotionTokens.cs
Normal 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";
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
Opacity="0.62"
|
||||
Stretch="UniformToFill">
|
||||
<Image.Effect>
|
||||
<BlurEffect Radius="42" />
|
||||
<BlurEffect Radius="{DynamicResource MotionBackdropBlurRadiusStrong}" />
|
||||
</Image.Effect>
|
||||
</Image>
|
||||
</Border>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user