using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; using FluentAvalonia.Styling; using LanMontainDesktop.Models; using LanMontainDesktop.Services; using LanMontainDesktop.Theme; using LibVLCSharp.Shared; namespace LanMontainDesktop.Views; public partial class MainWindow : Window { private enum WallpaperPlacement { Fill, Fit, Stretch, Center, Tile } private enum WallpaperMediaType { None, Image, Video } private const int StatusBarRowIndex = 0; private const int MinShortSideCells = 6; private const int MaxShortSideCells = 96; private const int SettingsTransitionDurationMs = 240; private const double LightBackgroundLuminanceThreshold = 0.57; private const string ClockStatusComponentId = "Clock"; private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle"; private static readonly HashSet SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp" }; private static readonly HashSet SupportedVideoExtensions = new(StringComparer.OrdinalIgnoreCase) { ".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v" }; private static readonly TaskbarActionId[] DefaultPinnedTaskbarActions = [ TaskbarActionId.MinimizeToWindows, TaskbarActionId.OpenSettings ]; private readonly record struct GridMetrics(int ColumnCount, int RowCount, double CellSize); private readonly MonetColorService _monetColorService = new(); private readonly AppSettingsService _appSettingsService = new(); private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme; private readonly HashSet _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _pinnedTaskbarActions = []; private int _targetShortSideCells; private bool _isSettingsOpen; private bool _isNightMode; private bool _enableDynamicTaskbarActions; private bool _suppressThemeToggleEvents; private bool _suppressStatusBarToggleEvents; private bool _suppressSettingsPersistence; private TranslateTransform? _settingsContentPanelTransform; private IBrush? _defaultDesktopBackground; private Bitmap? _wallpaperBitmap; private WallpaperMediaType _wallpaperMediaType; private string? _wallpaperVideoPath; private LibVLC? _libVlc; private MediaPlayer? _videoWallpaperPlayer; private Media? _videoWallpaperMedia; private MediaPlayer? _previewVideoWallpaperPlayer; private Media? _previewVideoWallpaperMedia; private string? _wallpaperPath; private string _wallpaperStatus = "Current background uses solid color."; private IReadOnlyList _recommendedColors = Array.Empty(); private IReadOnlyList _monetColors = Array.Empty(); private Color _selectedThemeColor = Color.Parse("#FF3B82F6"); private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle; public MainWindow() { InitializeComponent(); _fluentAvaloniaTheme = Application.Current?.Styles.OfType().FirstOrDefault(); PropertyChanged += OnWindowPropertyChanged; } protected override void OnOpened(EventArgs e) { base.OnOpened(e); _suppressSettingsPersistence = true; var snapshot = _appSettingsService.Load(); _targetShortSideCells = Math.Clamp( snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(), MinShortSideCells, MaxShortSideCells); GridSizeNumberBox.Value = _targetShortSideCells; SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 3); UpdateSettingsTabContent(); WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement); _defaultDesktopBackground = DesktopWallpaperLayer.Background; ApplyTaskbarSettings(snapshot); TryRestoreWallpaper(snapshot.WallpaperPath); ApplyWallpaperBrush(); UpdateWallpaperDisplay(); if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor)) { _selectedThemeColor = savedThemeColor; } _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold); ApplyNightModeState(_isNightMode, refreshPalettes: true); _suppressStatusBarToggleEvents = true; StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(ClockStatusComponentId); _suppressStatusBarToggleEvents = false; ThemeColorStatusTextBlock.Text = $"Theme color ready: {_selectedThemeColor}."; _settingsContentPanelTransform = SettingsContentPanel.RenderTransform as TranslateTransform; DesktopHost.SizeChanged += OnDesktopHostSizeChanged; WallpaperPreviewHost.SizeChanged += OnWallpaperPreviewHostSizeChanged; RebuildDesktopGrid(); _suppressSettingsPersistence = false; PersistSettings(); } protected override void OnClosed(EventArgs e) { PersistSettings(); StopVideoWallpaper(); _previewVideoWallpaperMedia?.Dispose(); _previewVideoWallpaperMedia = null; _previewVideoWallpaperPlayer?.Dispose(); _previewVideoWallpaperPlayer = null; _videoWallpaperMedia?.Dispose(); _videoWallpaperMedia = null; _videoWallpaperPlayer?.Dispose(); _videoWallpaperPlayer = null; _libVlc?.Dispose(); _libVlc = null; _wallpaperBitmap?.Dispose(); _wallpaperBitmap = null; PropertyChanged -= OnWindowPropertyChanged; DesktopHost.SizeChanged -= OnDesktopHostSizeChanged; WallpaperPreviewHost.SizeChanged -= OnWallpaperPreviewHostSizeChanged; base.OnClosed(e); } private int CalculateDefaultShortSideCellCountFromDpi() { var dpi = 96d * RenderScaling; var count = (int)Math.Round(dpi / 8d); return Math.Clamp(count, MinShortSideCells, MaxShortSideCells); } private void OnDesktopHostSizeChanged(object? sender, SizeChangedEventArgs e) { RebuildDesktopGrid(); PersistSettings(); } private void OnWallpaperPreviewHostSizeChanged(object? sender, SizeChangedEventArgs e) { UpdateWallpaperPreviewLayout(); } private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e) { var requested = (int)Math.Round(GridSizeNumberBox.Value); if (requested <= 0) { requested = _targetShortSideCells; } _targetShortSideCells = Math.Clamp(requested, MinShortSideCells, MaxShortSideCells); if (Math.Abs(GridSizeNumberBox.Value - _targetShortSideCells) > double.Epsilon) { GridSizeNumberBox.Value = _targetShortSideCells; } RebuildDesktopGrid(); } private void RebuildDesktopGrid() { var gridMetrics = CalculateGridMetrics( DesktopHost.Bounds.Width, DesktopHost.Bounds.Height, _targetShortSideCells); if (gridMetrics.CellSize <= 0) { return; } DesktopGrid.RowDefinitions.Clear(); DesktopGrid.ColumnDefinitions.Clear(); DesktopGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize; DesktopGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize; for (var row = 0; row < gridMetrics.RowCount; row++) { DesktopGrid.RowDefinitions.Add(new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); } for (var col = 0; col < gridMetrics.ColumnCount; col++) { DesktopGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); } PlaceStatusBarComponent( TopStatusBarHost, column: 0, requestedColumnSpan: gridMetrics.ColumnCount, totalColumns: gridMetrics.ColumnCount); var taskbarRow = gridMetrics.RowCount - 1; Grid.SetRow(BottomTaskbarContainer, taskbarRow); Grid.SetColumn(BottomTaskbarContainer, 0); Grid.SetRowSpan(BottomTaskbarContainer, 1); Grid.SetColumnSpan(BottomTaskbarContainer, gridMetrics.ColumnCount); ApplyTopStatusComponentVisibility(); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); ApplyWidgetSizing(gridMetrics.CellSize); GridInfoTextBlock.Text = $"Grid: {gridMetrics.ColumnCount} cols x {gridMetrics.RowCount} rows | cell {gridMetrics.CellSize:F1}px (1:1)"; UpdateWallpaperPreviewLayout(); } private static GridMetrics CalculateGridMetrics(double hostWidth, double hostHeight, int targetShortSideCells) { if (hostWidth <= 1 || hostHeight <= 1) { return default; } var shortSideCells = Math.Max(1, targetShortSideCells); if (hostWidth >= hostHeight) { var rowCount = shortSideCells; var cellSize = hostHeight / rowCount; var columnCount = Math.Max(1, (int)Math.Floor(hostWidth / cellSize)); return new GridMetrics(columnCount, rowCount, cellSize); } var columns = shortSideCells; var size = hostWidth / columns; var rows = Math.Max(1, (int)Math.Floor(hostHeight / size)); return new GridMetrics(columns, rows, size); } private static int ClampComponentSpan(int requestedSpan, int axisCellCount) { return Math.Clamp(requestedSpan, 1, Math.Max(1, axisCellCount)); } private static int ClampGridIndex(int requestedIndex, int axisCellCount) { return Math.Clamp(requestedIndex, 0, Math.Max(0, axisCellCount - 1)); } private static void PlaceStatusBarComponent( Control component, int column, int requestedColumnSpan, int totalColumns) { var clampedColumn = ClampGridIndex(column, totalColumns); var availableColumns = Math.Max(1, totalColumns - clampedColumn); Grid.SetRow(component, StatusBarRowIndex); Grid.SetColumn(component, clampedColumn); Grid.SetRowSpan(component, 1); Grid.SetColumnSpan(component, ClampComponentSpan(requestedColumnSpan, availableColumns)); } private void ApplyWidgetSizing(double cellSize) { var margin = Math.Clamp(cellSize * 0.08, 1.5, 10); var verticalPadding = Math.Clamp(cellSize * 0.08, 2, 12); var horizontalPadding = Math.Clamp(cellSize * 0.20, 4, 22); var taskbarCell = Math.Clamp(cellSize, 28, 128); TopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1.5, 10)); ClockWidget.Margin = new Thickness(margin); ClockWidget.ApplyCellSize(cellSize); BottomTaskbarContainer.Margin = new Thickness(Math.Clamp(cellSize * 0.18, 6, 18)); BottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.24, 10, 24)); BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 2, 10)); BackToWindowsContainer.Margin = new Thickness(0); BackToWindowsContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.16, 6, 16)); BackToWindowsButton.Padding = new Thickness(horizontalPadding, verticalPadding); BackToWindowsButton.FontSize = Math.Clamp(cellSize * 0.22, 8, 22); BackToWindowsButton.MinHeight = taskbarCell; BackToWindowsButton.MinWidth = Math.Clamp(cellSize * 2.3, 90, 320); OpenSettingsContainer.Margin = new Thickness(0); OpenSettingsContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.16, 6, 16)); OpenSettingsContainer.Width = taskbarCell; OpenSettingsContainer.Height = taskbarCell; OpenSettingsButton.Width = taskbarCell; OpenSettingsButton.Height = taskbarCell; OpenSettingsButton.Padding = new Thickness(Math.Clamp(taskbarCell * 0.2, 4, 12)); } private void UpdateWallpaperPreviewLayout() { if (WallpaperPreviewFrame is null || WallpaperPreviewHost is null || WallpaperPreviewGrid is null) { return; } var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width); var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height); var aspectRatio = desktopWidth / desktopHeight; var availableWidth = WallpaperPreviewHost.Bounds.Width - 20; var availableHeight = WallpaperPreviewHost.Bounds.Height - 20; if (availableWidth <= 1) { availableWidth = WallpaperPreviewFrame.Width; } if (availableHeight <= 1) { availableHeight = WallpaperPreviewFrame.Height; } availableWidth = Math.Max(1, availableWidth); availableHeight = Math.Max(1, availableHeight); var previewWidth = availableWidth; var previewHeight = previewWidth / aspectRatio; if (previewHeight > availableHeight) { previewHeight = availableHeight; previewWidth = previewHeight * aspectRatio; } WallpaperPreviewFrame.Width = previewWidth; WallpaperPreviewFrame.Height = previewHeight; WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm"); var gridMetrics = CalculateGridMetrics(previewWidth, previewHeight, _targetShortSideCells); if (gridMetrics.CellSize <= 0) { return; } WallpaperPreviewGrid.RowDefinitions.Clear(); WallpaperPreviewGrid.ColumnDefinitions.Clear(); WallpaperPreviewGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize; WallpaperPreviewGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize; for (var row = 0; row < gridMetrics.RowCount; row++) { WallpaperPreviewGrid.RowDefinitions.Add( new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); } for (var col = 0; col < gridMetrics.ColumnCount; col++) { WallpaperPreviewGrid.ColumnDefinitions.Add( new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); } PlaceStatusBarComponent( WallpaperPreviewTopStatusBarHost, column: 0, requestedColumnSpan: gridMetrics.ColumnCount, totalColumns: gridMetrics.ColumnCount); var taskbarRow = gridMetrics.RowCount - 1; Grid.SetRow(WallpaperPreviewBottomTaskbarContainer, taskbarRow); Grid.SetColumn(WallpaperPreviewBottomTaskbarContainer, 0); Grid.SetRowSpan(WallpaperPreviewBottomTaskbarContainer, 1); Grid.SetColumnSpan(WallpaperPreviewBottomTaskbarContainer, gridMetrics.ColumnCount); ApplyTopStatusComponentVisibility(); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); ApplyPreviewWidgetSizing(gridMetrics.CellSize); } private void ApplyPreviewWidgetSizing(double cellSize) { var margin = Math.Clamp(cellSize * 0.08, 1, 6); var previewTaskbarCell = Math.Clamp(cellSize, 10, 36); WallpaperPreviewTopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1, 4)); WallpaperPreviewBottomTaskbarContainer.Margin = new Thickness(margin); WallpaperPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.22, 4, 10)); WallpaperPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4)); WallpaperPreviewClockTextBlock.FontSize = Math.Clamp(cellSize * 0.30, 6, 18); WallpaperPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13); WallpaperPreviewBackButtonContainer.MinHeight = previewTaskbarCell; WallpaperPreviewBackButtonContainer.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120); WallpaperPreviewSettingsButtonContainer.Width = previewTaskbarCell; WallpaperPreviewSettingsButtonContainer.Height = previewTaskbarCell; WallpaperPreviewSettingsButtonIcon.Width = Math.Clamp(previewTaskbarCell * 0.42, 6, 14); WallpaperPreviewSettingsButtonIcon.Height = Math.Clamp(previewTaskbarCell * 0.42, 6, 14); var cornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.12, 3, 10)); WallpaperPreviewBackButtonContainer.CornerRadius = cornerRadius; WallpaperPreviewSettingsButtonContainer.CornerRadius = cornerRadius; } private void OnMinimizeClick(object? sender, RoutedEventArgs e) { WindowState = WindowState.Minimized; } private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property != WindowStateProperty) { return; } if (WindowState is WindowState.Minimized or WindowState.FullScreen) { return; } Dispatcher.UIThread.Post(() => { if (WindowState is not (WindowState.Minimized or WindowState.FullScreen)) { WindowState = WindowState.FullScreen; } }); } }