diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 7f268fd..dfa681e 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -481,9 +481,9 @@ public partial class App : Application RestoreOrCreateMainWindow(showSingleInstanceNotice: true, source: "SingleInstance"); } - private void RestoreOrCreateMainWindow(bool showSingleInstanceNotice, string source) + private async void RestoreOrCreateMainWindow(bool showSingleInstanceNotice, string source) { - Dispatcher.UIThread.Post(() => + Dispatcher.UIThread.Post(async () => { if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) { @@ -498,6 +498,13 @@ public partial class App : Application if (!mainWindow.IsVisible) { mainWindow.Show(); + if (mainWindow._isFirstLaunchAfterOpen) + { + mainWindow._isFirstLaunchAfterOpen = false; + mainWindow.ForceDesktopPageToFirst(); + } + + await mainWindow.SlideInAsync(); } if (mainWindow.WindowState == WindowState.Minimized) @@ -879,10 +886,11 @@ public partial class App : Application SetDesktopShellState(DesktopShellState.ForegroundDesktop, "MainWindowRestored"); } - private void HideMainWindowToTray(MainWindow mainWindow, string source) + private async void HideMainWindowToTray(MainWindow mainWindow, string source) { try { + await mainWindow.SlideOutAsync(); mainWindow.ShowInTaskbar = false; mainWindow.Hide(); SetDesktopShellState(DesktopShellState.TrayOnly, source); diff --git a/LanMountainDesktop/Behaviors/WindowSlideAnimationBehavior.cs b/LanMountainDesktop/Behaviors/WindowSlideAnimationBehavior.cs new file mode 100644 index 0000000..326bb17 --- /dev/null +++ b/LanMountainDesktop/Behaviors/WindowSlideAnimationBehavior.cs @@ -0,0 +1,147 @@ +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.Threading; +using LanMountainDesktop.Theme; + +namespace LanMountainDesktop.Behaviors; + +public static class WindowSlideAnimationBehavior +{ + private static readonly Easing DecelerateEasing = Easing.Parse(FluttermotionToken.StandardBezier); + private static readonly Easing AccelerateEasing = new CubicEaseIn(); + + public static readonly TimeSpan SlideInDuration = TimeSpan.FromMilliseconds(350); + public static readonly TimeSpan SlideOutDuration = TimeSpan.FromMilliseconds(280); + + public static async Task SlideInAsync(Window window, Border desktopHost) + { + if (window is null || desktopHost is null) + { + return; + } + + var screenWidth = Math.Max(1, window.Bounds.Width > 1 ? window.Bounds.Width : PrimaryScreenWidth(window)); + var transform = EnsureTranslateTransform(desktopHost); + + transform.X = screenWidth; + desktopHost.Opacity = 1; + window.Show(); + + if (screenWidth <= 1) + { + transform.X = 0; + return; + } + + var animation = new Animation + { + Duration = SlideInDuration, + Easing = DecelerateEasing, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = { new Setter(TranslateTransform.XProperty, screenWidth) } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = { new Setter(TranslateTransform.XProperty, 0d) } + } + } + }; + + await animation.RunAsync(desktopHost); + } + + public static async Task SlideOutAsync(Window window, Border desktopHost, Action? onCompleted = null) + { + if (window is null || desktopHost is null) + { + onCompleted?.Invoke(); + return; + } + + var screenWidth = Math.Max(1, window.Bounds.Width > 1 ? window.Bounds.Width : PrimaryScreenWidth(window)); + var transform = EnsureTranslateTransform(desktopHost); + + if (screenWidth <= 1) + { + onCompleted?.Invoke(); + return; + } + + var animation = new Animation + { + Duration = SlideOutDuration, + Easing = AccelerateEasing, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = { new Setter(TranslateTransform.XProperty, 0d) } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = { new Setter(TranslateTransform.XProperty, screenWidth) } + } + } + }; + + await animation.RunAsync(desktopHost); + onCompleted?.Invoke(); + } + + public static void ResetSlidePosition(Border desktopHost) + { + if (desktopHost is null) + { + return; + } + + var transform = desktopHost.RenderTransform as TranslateTransform; + if (transform is not null) + { + transform.X = 0; + } + + desktopHost.Opacity = 1; + } + + private static TranslateTransform EnsureTranslateTransform(Border desktopHost) + { + if (desktopHost.RenderTransform is TranslateTransform existingTransform) + { + return existingTransform; + } + + var newTransform = new TranslateTransform(); + desktopHost.RenderTransform = newTransform; + return newTransform; + } + + private static double PrimaryScreenWidth(Window window) + { + try + { + if (window.Screens?.Primary is { } screen) + { + return screen.WorkingArea.Width; + } + } + catch + { + } + + return 1920; + } +} diff --git a/LanMountainDesktop/Views/Components/ZhiJiaoHubWidget.axaml.cs b/LanMountainDesktop/Views/Components/ZhiJiaoHubWidget.axaml.cs index 4d11b6c..7d634e5 100644 --- a/LanMountainDesktop/Views/Components/ZhiJiaoHubWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ZhiJiaoHubWidget.axaml.cs @@ -47,6 +47,10 @@ public partial class ZhiJiaoHubWidget : UserControl, private bool _autoRefreshEnabled = true; private int _pendingImageIndex = 0; + private string _lastLoadedSource = string.Empty; + private bool _lastLoadedAutoRefreshEnabled = true; + private int _lastLoadedRefreshIntervalMinutes = 30; + private IReadOnlyList _images = []; private int _currentImageIndex = 0; @@ -147,11 +151,39 @@ public partial class ZhiJiaoHubWidget : UserControl, _placementId = context.PlacementId ?? string.Empty; _componentSettingsAccessor = context.ComponentSettingsAccessor; - LoadSettings(); - - if (_isAttached) + try { - _ = InitializeAsync(); + var snapshot = _componentSettingsAccessor?.LoadSnapshot(); + LoadSettings(); + + if (_isAttached) + { + if (snapshot is not null && NeedsReinitialization(snapshot)) + { + _ = InitializeAsync(); + } + else if (_images.Count > 0) + { + _pendingImageIndex = snapshot?.ZhiJiaoHubCurrentImageIndex ?? 0; + _currentImageIndex = Math.Clamp(_pendingImageIndex, 0, Math.Max(0, _images.Count - 1)); + _pendingImageIndex = 0; + if (TryDisplayCachedImage(_currentImageIndex)) + { + UpdateIndicators(); + } + } + else + { + _ = InitializeAsync(); + } + } + } + catch + { + if (_isAttached) + { + _ = InitializeAsync(); + } } } @@ -163,11 +195,28 @@ public partial class ZhiJiaoHubWidget : UserControl, public void RefreshFromSettings() { - LoadSettings(); - UpdateTimers(); - if (_isAttached) + try + { + var snapshot = _componentSettingsAccessor?.LoadSnapshot(); + if (snapshot is null) + { + return; + } + + LoadSettings(); + UpdateTimers(); + + if (_isAttached && NeedsReinitialization(snapshot)) + { + _ = InitializeAsync(); + } + else + { + _pendingImageIndex = snapshot.ZhiJiaoHubCurrentImageIndex; + } + } + catch { - _ = InitializeAsync(); } } @@ -192,6 +241,24 @@ public partial class ZhiJiaoHubWidget : UserControl, } } + private bool NeedsReinitialization(ComponentSettingsSnapshot snapshot) + { + var newSource = ZhiJiaoHubSources.Normalize(snapshot.ZhiJiaoHubSource); + var newAutoRefreshEnabled = snapshot.ZhiJiaoHubAutoRefreshEnabled; + var newRefreshIntervalMinutes = Math.Clamp(snapshot.ZhiJiaoHubAutoRefreshIntervalMinutes, 5, 1440); + + return newSource != _lastLoadedSource || + newAutoRefreshEnabled != _lastLoadedAutoRefreshEnabled || + newRefreshIntervalMinutes != _lastLoadedRefreshIntervalMinutes; + } + + private void UpdateLastLoadedSettings(ComponentSettingsSnapshot snapshot) + { + _lastLoadedSource = ZhiJiaoHubSources.Normalize(snapshot.ZhiJiaoHubSource); + _lastLoadedAutoRefreshEnabled = snapshot.ZhiJiaoHubAutoRefreshEnabled; + _lastLoadedRefreshIntervalMinutes = Math.Clamp(snapshot.ZhiJiaoHubAutoRefreshIntervalMinutes, 5, 1440); + } + private void SaveCurrentImageIndex() { try @@ -259,6 +326,12 @@ public partial class ZhiJiaoHubWidget : UserControl, _currentImageIndex = Math.Clamp(_pendingImageIndex, 0, Math.Max(0, _images.Count - 1)); _pendingImageIndex = 0; + var snapshot = _componentSettingsAccessor?.LoadSnapshot(); + if (snapshot is not null) + { + UpdateLastLoadedSettings(snapshot); + } + await Dispatcher.UIThread.InvokeAsync(() => { UpdateIndicators(); diff --git a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs index 825c608..46f40bd 100644 --- a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs +++ b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -375,6 +375,18 @@ public partial class MainWindow UpdateDesktopPageAwareComponentContext(); } + public void ForceDesktopPageToFirst() + { + if (_currentDesktopSurfaceIndex == 0) + { + return; + } + + _currentDesktopSurfaceIndex = 0; + ApplyDesktopSurfaceOffset(); + SchedulePersistSettings(delayMs: 120); + } + private void SetDesktopPagesHostSnapAnimationEnabled(bool enabled) { if (_desktopPagesHostTransform is null) diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs index ac27f93..58bd638 100644 --- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs +++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs @@ -38,6 +38,12 @@ public partial class MainWindow return; } + // 组件实例范围的设置变更不应触发整个桌面重新加载(比如翻页保存图片索引) + if (e.Scope == SettingsScope.ComponentInstance) + { + return; + } + if (e.Scope == SettingsScope.App && e.ChangedKeys is { Count: > 0 }) { var changedKeys = e.ChangedKeys.ToArray(); diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs index abbf02f..16c3f39 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml.cs +++ b/LanMountainDesktop/Views/MainWindow.axaml.cs @@ -18,6 +18,7 @@ using Avalonia.Styling; using Avalonia.Threading; using Avalonia.VisualTree; using FluentAvalonia.Styling; +using LanMountainDesktop.Behaviors; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; @@ -108,6 +109,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private bool _suppressWeatherLocationEvents; private bool _suppressSettingsPersistence; private bool _isComponentLibraryOpen; + private bool _isSlideAnimating; + private int _slideAnimationGuard; + internal bool _isFirstLaunchAfterOpen = true; private Border? _selectedDesktopComponentHost; private bool _reopenSettingsAfterComponentLibraryClose; private TranslateTransform? _settingsContentPanelTransform; @@ -785,9 +789,60 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider _ = cellSize; } - private void OnMinimizeClick(object? sender, RoutedEventArgs e) + private async void OnMinimizeClick(object? sender, RoutedEventArgs e) { - WindowState = WindowState.Minimized; + if (_isSlideAnimating) + { + return; + } + + await SlideOutAsync(); + } + + public async Task SlideInAsync() + { + if (_isSlideAnimating || DesktopHost is null) + { + return; + } + + var guard = System.Threading.Interlocked.Increment(ref _slideAnimationGuard); + _isSlideAnimating = true; + + try + { + await WindowSlideAnimationBehavior.SlideInAsync(this, DesktopHost); + } + finally + { + _isSlideAnimating = false; + System.Threading.Interlocked.Decrement(ref _slideAnimationGuard); + } + } + + public async Task SlideOutAsync() + { + if (_isSlideAnimating || DesktopHost is null) + { + return; + } + + var guard = System.Threading.Interlocked.Increment(ref _slideAnimationGuard); + _isSlideAnimating = true; + + try + { + await WindowSlideAnimationBehavior.SlideOutAsync(this, DesktopHost, () => + { + WindowState = WindowState.Minimized; + WindowSlideAnimationBehavior.ResetSlidePosition(DesktopHost!); + }); + } + finally + { + _isSlideAnimating = false; + System.Threading.Interlocked.Decrement(ref _slideAnimationGuard); + } } private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)