using FluidExplorer.Services; using FluidExplorer.Services.Operations; using FluidExplorer.ViewModels; using Microsoft.UI; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using Windows.Graphics; using Windows.System; namespace FluidExplorer; public sealed partial class MainWindow : Window { private readonly AppServices _services; private readonly AppSettings _settings; public MainWindow(AppServices services) { _services = services; _settings = services.Settings; ViewModel = services.Main; InitializeComponent(); Shell.ViewModel = ViewModel; Title = "文件资源管理器"; SystemBackdrop = new MicaBackdrop(); ExtendsContentIntoTitleBar = true; SetTitleBar(Shell.DragRegion); RestoreWindowPlacement(); ApplyCaptionButtonColors(); HookServices(); Shell.ThemedRoot.ActualThemeChanged += (_, _) => ApplyCaptionButtonColors(); Shell.SizeChanged += (_, _) => UpdateTitleBarInsets(); AppWindow.Changed += (_, _) => UpdateTitleBarInsets(); AppWindow.Closing += OnClosing; RegisterAccelerators(); // 启动:恢复标签页 + 建立索引(都在后台,窗口先出来) _ = DispatcherQueue.TryEnqueue(async () => await ViewModel.InitializeAsync()); } public MainViewModel ViewModel { get; } public void ShowOperationQueue() => Shell.ShowOperationQueue(); // ── 窗口外观 ──────────────────────────────────────────────────────────── private void RestoreWindowPlacement() { if (!double.IsNaN(_settings.WindowWidth) && _settings.WindowWidth > 400) { AppWindow.ResizeClient(new SizeInt32((int)_settings.WindowWidth, (int)_settings.WindowHeight)); } if (!double.IsNaN(_settings.WindowLeft) && !double.IsNaN(_settings.WindowTop) && IsOnScreen(_settings.WindowLeft, _settings.WindowTop)) { AppWindow.Move(new PointInt32((int)_settings.WindowLeft, (int)_settings.WindowTop)); } if (_settings.WindowMaximized && AppWindow.Presenter is OverlappedPresenter presenter) { presenter.Maximize(); } UpdateTitleBarInsets(); } private static bool IsOnScreen(double left, double top) { var display = DisplayArea.GetFromPoint(new PointInt32((int)left, (int)top), DisplayAreaFallback.Nearest); return display is not null && left >= display.WorkArea.X - 40 && top >= display.WorkArea.Y - 40; } /// 把标签栏右侧的留白让给系统标题栏按钮,保证标签不被按钮压住。 private void UpdateTitleBarInsets() { try { var scale = Shell.XamlRoot?.RasterizationScale ?? 1.0; var rightInset = AppWindow.TitleBar.RightInset / scale; if (rightInset < 0) rightInset = 0; if (Shell.DragRegion is FrameworkElement region) region.Margin = new Thickness(0, 0, rightInset, 0); } catch { // 最小化/全屏切换等瞬态下取不到 TitleBar 信息,忽略 } } /// 标题栏按钮颜色跟随当前主题。 private void ApplyCaptionButtonColors() { var titleBar = AppWindow.TitleBar; titleBar.ButtonBackgroundColor = Colors.Transparent; titleBar.ButtonInactiveBackgroundColor = Colors.Transparent; if (Shell.ThemedRoot.ActualTheme == ElementTheme.Dark) { titleBar.ButtonForegroundColor = Colors.White; titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(24, 255, 255, 255); titleBar.ButtonHoverForegroundColor = Colors.White; titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(48, 255, 255, 255); titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(200, 255, 255, 255); titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(120, 255, 255, 255); } else { titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(230, 0, 0, 0); titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(20, 0, 0, 0); titleBar.ButtonHoverForegroundColor = Colors.Black; titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(40, 0, 0, 0); titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(160, 0, 0, 0); titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(100, 0, 0, 0); } } /// 主题切换(用户手动指定时覆盖系统跟随)。 public void ApplyThemeMode(AppThemeMode mode) { Shell.ThemedRoot.RequestedTheme = mode switch { AppThemeMode.Light => ElementTheme.Light, AppThemeMode.Dark => ElementTheme.Dark, _ => ElementTheme.Default }; } // ── 服务接线 ──────────────────────────────────────────────────────────── private void HookServices() { _services.Operations.ConflictResolver = ShowConflictDialogAsync; ViewModel.ThemeModeChanged += (_, mode) => ApplyThemeMode(mode); ViewModel.AnimationsToggled += (_, enabled) => { // 关掉动画时禁用所有依赖动画(WinUI 原生总开关,克制且彻底) Microsoft.UI.Xaml.Media.Animation.Timeline.AllowDependentAnimations = enabled; }; ViewModel.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(MainViewModel.ErrorMessage) && ViewModel.ErrorMessage is { Length: > 0 } message) { ViewModel.ActivePane?.ReportError(message); ViewModel.ErrorMessage = null; } }; } /// 文件冲突对话框:不弹系统模态框,全部由应用自己处理。 private async Task ShowConflictDialogAsync(ConflictInfo info) { var dialog = new ContentDialog { XamlRoot = Shell.XamlRoot, Title = "目标位置已有同名项目", DefaultButton = ContentDialogButton.Primary }; var keepBoth = new RadioButton { Content = "同时保留两个文件(推荐)", IsChecked = true }; var replace = new RadioButton { Content = "替换目标中的文件" }; var skip = new RadioButton { Content = "跳过此文件" }; var applyAll = new CheckBox { Content = "为后续所有冲突执行相同操作" }; var detail = new StackPanel { Spacing = 8 }; detail.Children.Add(new TextBlock { Text = Path.GetFileName(info.SourcePath), TextWrapping = TextWrapping.Wrap, Style = (Style)Application.Current.Resources["BodyStrongTextBlockStyle"] }); detail.Children.Add(new TextBlock { Text = $"源:{info.SourcePath}\n{Models.FileEntry.FormatSize(info.SourceSize)} · {info.SourceModifiedUtc.ToLocalTime():yyyy/MM/dd HH:mm}\n\n" + $"目标:{info.DestinationPath}\n{Models.FileEntry.FormatSize(info.DestinationSize)} · {info.DestinationModifiedUtc.ToLocalTime():yyyy/MM/dd HH:mm}", TextWrapping = TextWrapping.Wrap, Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"] }); detail.Children.Add(keepBoth); detail.Children.Add(replace); detail.Children.Add(skip); detail.Children.Add(applyAll); dialog.Content = detail; dialog.PrimaryButtonText = "继续"; dialog.CloseButtonText = "取消本次操作"; var result = await dialog.ShowAsync(); info.ApplyToAll = applyAll.IsChecked == true; if (result != ContentDialogResult.Primary) return ConflictResolution.Cancel; if (replace.IsChecked == true) return ConflictResolution.Replace; if (skip.IsChecked == true) return ConflictResolution.Skip; return ConflictResolution.KeepBoth; } // ── 快捷键 ────────────────────────────────────────────────────────────── private void RegisterAccelerators() { AddAccelerator(VirtualKey.T, VirtualKeyModifiers.Control, () => ViewModel.NewTabCommand.Execute(null)); AddAccelerator(VirtualKey.W, VirtualKeyModifiers.Control, () => ViewModel.CloseTabCommand.Execute(ViewModel.SelectedTab)); AddAccelerator(VirtualKey.Z, VirtualKeyModifiers.Control, () => ViewModel.UndoCommand.Execute(null)); AddAccelerator(VirtualKey.D, VirtualKeyModifiers.Control | VirtualKeyModifiers.Shift, () => ViewModel.ToggleDualPaneCommand.Execute(null)); AddAccelerator(VirtualKey.F, VirtualKeyModifiers.Control, () => ViewModel.ActivePane?.RequestSearchFocus()); AddAccelerator(VirtualKey.Tab, VirtualKeyModifiers.Control, CycleTab); } private void AddAccelerator(VirtualKey key, VirtualKeyModifiers modifiers, Action action) { var accelerator = new KeyboardAccelerator { Key = key, Modifiers = modifiers }; accelerator.Invoked += (_, args) => { // 文本输入状态下不抢 Ctrl+Z / Ctrl+F if (key is VirtualKey.Z or VirtualKey.F && modifiers == VirtualKeyModifiers.Control && FocusManager.GetFocusedElement(Shell.XamlRoot) is TextBox) { return; } args.Handled = true; action(); }; Shell.ThemedRoot.KeyboardAccelerators.Add(accelerator); } private void CycleTab() { if (ViewModel.Tabs.Count < 2 || ViewModel.SelectedTab is null) return; var index = ViewModel.Tabs.IndexOf(ViewModel.SelectedTab); ViewModel.SelectedTab = ViewModel.Tabs[(index + 1) % ViewModel.Tabs.Count]; } // ── 关闭 ──────────────────────────────────────────────────────────────── private void OnClosing(AppWindow sender, AppWindowClosingEventArgs args) { try { // 有正在进行的复制/移动时不静默退出,避免用户以为文件已搬完 var hasActiveJob = _services.Operations.Jobs.Any(j => !j.IsFinished); if (hasActiveJob && !_settings.KeepWindowOpenDuringOperations) { args.Cancel = true; return; } ViewModel.SaveOpenTabs(); // 索引持有卷句柄,退出时显式释放 foreach (var index in _services.Search.AllIndexes) { if (index is IDisposable disposable) disposable.Dispose(); } _settings.WindowMaximized = AppWindow.Presenter is OverlappedPresenter { State: OverlappedPresenterState.Maximized }; if (!_settings.WindowMaximized) { _settings.WindowLeft = AppWindow.Position.X; _settings.WindowTop = AppWindow.Position.Y; _settings.WindowWidth = AppWindow.Size.Width; _settings.WindowHeight = AppWindow.Size.Height; } _settings.Save(); } catch { // 关闭时保存失败不应阻塞退出 } } }