270 lines
12 KiB
C#
270 lines
12 KiB
C#
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;
|
|
}
|
|
|
|
/// <summary>把标签栏右侧的留白让给系统标题栏按钮,保证标签不被按钮压住。</summary>
|
|
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 信息,忽略
|
|
}
|
|
}
|
|
|
|
/// <summary>标题栏按钮颜色跟随当前主题。</summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>主题切换(用户手动指定时覆盖系统跟随)。</summary>
|
|
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;
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>文件冲突对话框:不弹系统模态框,全部由应用自己处理。</summary>
|
|
private async Task<ConflictResolution> 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
|
|
{
|
|
// 关闭时保存失败不应阻塞退出
|
|
}
|
|
}
|
|
}
|