using System.Collections.ObjectModel; using System.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluidExplorer.Navigation; using FluidExplorer.Services; using FluidExplorer.Services.FileSystem; using FluidExplorer.Services.Icons; using FluidExplorer.Services.ItemVisuals; using FluidExplorer.Services.Operations; using FluidExplorer.Services.Search; using FluidExplorer.Services.Shell; using FluidExplorer.Helpers; using Microsoft.UI.Dispatching; using PathHelper = FluidExplorer.Services.FileSystem.PathHelper; namespace FluidExplorer.ViewModels; /// 主视图模型:标签页、侧边栏、索引状态、操作队列、设置。 public sealed partial class MainViewModel : ObservableObject { private readonly IFileSystemService _fs; private readonly SearchService _search; private readonly IFileOperationService _operations; private readonly AppSettings _settings; private readonly DispatcherQueue _ui; public MainViewModel( IFileSystemService fs, SearchService search, IFileOperationService operations, IIconService icons, AppSettings settings, DispatcherQueue ui) { _fs = fs; _search = search; _operations = operations; _settings = settings; _ui = ui; Clipboard = new FileClipboard(); Visuals = new ItemVisualService(icons, ui); ShowHiddenFiles = settings.ShowHiddenFiles; ShowSystemFiles = settings.ShowSystemFiles; ShowFileExtensions = settings.ShowFileExtensions; AlwaysShowCheckBoxes = settings.AlwaysShowCheckBoxes; DeleteToRecycleBin = settings.DeleteToRecycleBin; AnimationsEnabled = settings.AnimationsEnabled; SearchInCurrentFolderOnly = settings.SearchScope == SearchScope.CurrentFolder; ThemeMode = settings.ThemeMode; _operations.JobsChanged += (_, _) => RefreshJobs(); _search.IndexStateChanged += (_, _) => _ui.Post(RefreshIndexState); _jobTimer = _ui.CreateTimer(); _jobTimer.Interval = TimeSpan.FromMilliseconds(250); _jobTimer.IsRepeating = true; _jobTimer.Tick += OnJobTimerTick; BuildSidebar(); RefreshIndexState(); RefreshJobs(); } public ItemVisualService Visuals { get; } public IFileClipboard Clipboard { get; } public AppSettings Settings => _settings; // ── 标签页 ────────────────────────────────────────────────────────────── public ObservableCollection Tabs { get; } = []; [ObservableProperty] private ExplorerTabViewModel? _selectedTab; public ExplorerPaneViewModel? ActivePane => SelectedTab?.ActivePane; public ExplorerTabViewModel AddTab(NavigationLocation? start = null, bool select = true) { var location = start ?? DefaultStartLocation(); var tab = new ExplorerTabViewModel(_fs, _search, _operations, Visuals, Clipboard, _settings, _ui, location); tab.PropertyChanged += (_, _) => { if (ReferenceEquals(tab, SelectedTab)) OnPropertyChanged(nameof(ActivePane)); }; Tabs.Add(tab); if (select) SelectedTab = tab; SaveOpenTabs(); return tab; } [RelayCommand] private void NewTab() => AddTab(); [RelayCommand] private void CloseTab(ExplorerTabViewModel? tab) { tab ??= SelectedTab; if (tab is null) return; var index = Tabs.IndexOf(tab); tab.Dispose(); Tabs.Remove(tab); if (Tabs.Count == 0) { AddTab(); return; } SelectedTab = Tabs[Math.Clamp(index, 0, Tabs.Count - 1)]; SaveOpenTabs(); } [RelayCommand] private void DuplicateTab(ExplorerTabViewModel? tab) { tab ??= SelectedTab; if (tab is null) return; AddTab(NavigationLocation.FromPath(tab.Primary.CurrentPath)); } [RelayCommand] private void ToggleDualPane() { SelectedTab?.ToggleDualPane(); _settings.DualPane = SelectedTab?.IsDualPane ?? false; _settings.Save(); } partial void OnSelectedTabChanged(ExplorerTabViewModel? value) { OnPropertyChanged(nameof(ActivePane)); if (value is not null) UpdateSidebarSelection(value.ActivePane.CurrentPath); } private NavigationLocation DefaultStartLocation() { if (!string.IsNullOrWhiteSpace(_settings.DefaultStartPath) && Directory.Exists(_settings.DefaultStartPath)) return NavigationLocation.FromPath(_settings.DefaultStartPath); var downloads = KnownFolders.Downloads; return Directory.Exists(downloads) ? NavigationLocation.FromPath(downloads) : NavigationLocation.Home; } // ── 侧边栏 ────────────────────────────────────────────────────────────── public ObservableCollection SidebarRoots { get; } = []; [ObservableProperty] private SidebarNode? _selectedSidebarNode; private void BuildSidebar() { SidebarRoots.Clear(); SidebarRoots.Add(SidebarNode.Create("主页", "\uE80F", NavigationLocation.Home)); SidebarRoots.Add(SidebarNode.Create("图库", "\uE91B", NavigationLocation.Gallery)); if (_settings.PinnedFolders.Count == 0) { foreach (var path in new[] { KnownFolders.Desktop, KnownFolders.Downloads, KnownFolders.Documents, KnownFolders.Pictures }) if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path)) _settings.PinnedFolders.Add(path); } var quick = SidebarNode.Create("快速访问", "\uE8B7", NavigationLocation.Home, expandable: true); quick.IsExpanded = true; foreach (var path in _settings.PinnedFolders) { if (!Directory.Exists(path)) continue; quick.Children.Add(SidebarNode.Create(PathHelper.GetName(path), "\uE8B7", NavigationLocation.FromPath(path), canPin: true)); } SidebarRoots.Add(quick); var thisPc = SidebarNode.Create("此电脑", "\uE977", NavigationLocation.ThisPc, expandable: true); thisPc.IsExpanded = true; foreach (var drive in DriveItem.Enumerate()) { thisPc.Children.Add(SidebarNode.Create(drive.DisplayName, drive.Glyph, new NavigationLocation(LocationKind.Drive, drive.RootPath, drive.DisplayName, drive.Glyph))); } SidebarRoots.Add(thisPc); SidebarRoots.Add(SidebarNode.Create("网络", "\uE968", NavigationLocation.Network)); SidebarRoots.Add(SidebarNode.Create("回收站", "\uE74D", NavigationLocation.RecycleBin)); } public void UpdateSidebarSelection(string path) { if (string.IsNullOrEmpty(path)) return; foreach (var node in EnumerateNodes(SidebarRoots)) { var match = node.Location.Kind switch { LocationKind.Home or LocationKind.Gallery or LocationKind.ThisPc or LocationKind.Network or LocationKind.RecycleBin => string.Equals(node.Location.Path, path, StringComparison.OrdinalIgnoreCase), LocationKind.Drive => string.Equals(node.Location.Path.TrimEnd('\\'), path.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase), _ => false }; if (match) { SelectedSidebarNode = node; return; } } } private static IEnumerable EnumerateNodes(IEnumerable nodes) { foreach (var node in nodes) { yield return node; foreach (var child in EnumerateNodes(node.Children)) yield return child; } } [RelayCommand] private async Task NavigateSidebarAsync(SidebarNode? node) { if (node is null) return; var pane = ActivePane ?? AddTab(node.Location).Primary; await pane.NavigateAsync(node.Location); UpdateSidebarSelection(node.Location.Path); } [RelayCommand] private void PinFolder() { var pane = ActivePane; if (pane is null || pane.Location.Kind is not (LocationKind.Folder or LocationKind.Drive)) return; if (_settings.PinnedFolders.Contains(pane.CurrentPath, StringComparer.OrdinalIgnoreCase)) return; _settings.PinnedFolders.Add(pane.CurrentPath); _settings.Save(); BuildSidebar(); } [RelayCommand] private void UnpinFolder(SidebarNode? node) { if (node is null) return; _settings.PinnedFolders.RemoveAll(p => string.Equals(p, node.Location.Path, StringComparison.OrdinalIgnoreCase)); _settings.Save(); BuildSidebar(); } // ── 索引状态 ──────────────────────────────────────────────────────────── [ObservableProperty] private string _indexSummary = "索引:未启动"; [ObservableProperty] private bool _isIndexing; [ObservableProperty] private double _indexProgress; [ObservableProperty] private bool _indexNeedsElevation; [ObservableProperty] private string _indexDetail = string.Empty; private void RefreshIndexState() { var state = _search.AggregateState; var count = _search.TotalIndexedEntries; IndexDetail = _search.AllIndexes.FirstOrDefault() is { } first ? first.GetType().Name : string.Empty; switch (state) { case IndexState.NotStarted: IndexSummary = "索引:未启动(搜索将退化为实时扫描)"; IsIndexing = false; break; case IndexState.Building: IndexSummary = $"索引:正在建立…(已索引 {count:N0} 项)"; IsIndexing = true; break; case IndexState.RequiresElevation: IndexSummary = "索引:需要管理员权限才能读取 NTFS 主文件表"; IsIndexing = false; IndexNeedsElevation = true; break; case IndexState.Failed: IndexSummary = "索引:不可用(该卷不是 NTFS)"; IsIndexing = false; break; default: IndexSummary = $"索引:{count:N0} 项 · 已就绪"; IsIndexing = false; IndexNeedsElevation = false; break; } } [RelayCommand] private async Task BuildIndexAsync() { if (IsIndexing) return; IsIndexing = true; IndexSummary = "索引:正在建立…"; var volumes = DriveItem.Enumerate() .Where(d => d.DriveType == 3 && d.IsReady) .Select(d => d.RootPath) .ToList(); if (volumes.Count == 0) volumes.Add(Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\"); _settings.IndexedVolumes = volumes; _settings.Save(); var progress = new Progress<(string Volume, double Progress)>(p => _ui.Post(() => { IndexProgress = p.Progress; IndexSummary = $"索引:正在读取 {p.Volume} … {p.Progress:P0}"; })); try { await _search.BuildIndexesAsync(volumes, progress, CancellationToken.None); } catch (Exception ex) { IndexSummary = $"索引:失败({ex.Message})"; } finally { IsIndexing = false; RefreshIndexState(); } } [RelayCommand] private void RestartElevated() { try { var exe = Environment.ProcessPath; if (string.IsNullOrEmpty(exe)) return; Process.Start(new ProcessStartInfo(exe) { UseShellExecute = true, Verb = "runas" }); Microsoft.UI.Xaml.Application.Current.Exit(); } catch { // 用户拒绝了 UAC } } // ── 操作队列 ──────────────────────────────────────────────────────────── public ObservableCollection JobRows { get; } = []; [ObservableProperty] private bool _isQueueExpanded; [ObservableProperty] private int _activeJobCount; private readonly DispatcherQueueTimer _jobTimer; private void RefreshJobs() { _ui.Post(() => { JobRows.Clear(); foreach (var job in _operations.Jobs) JobRows.Add(new JobRowViewModel(job)); ActiveJobCount = JobRows.Count(j => !j.IsFinished); OnPropertyChanged(nameof(HasJobs)); // 有活动作业时以 250ms 刷新快照(作业本身在后台线程更新,不能直接绑 UI) if (ActiveJobCount > 0) _jobTimer.Start(); else _jobTimer.Stop(); }); } private void OnJobTimerTick(DispatcherQueueTimer sender, object args) { var anyActive = false; foreach (var row in JobRows) { row.Refresh(); if (!row.IsFinished) anyActive = true; } ActiveJobCount = JobRows.Count(r => !r.IsFinished); if (!anyActive) sender.Stop(); } public bool HasJobs => JobRows.Count > 0; [RelayCommand] private void PauseJob(JobRowViewModel? row) { if (row is null) return; if (row.Job.State == JobState.Paused) _operations.Resume(row.Job.Id); else _operations.Pause(row.Job.Id); } [RelayCommand] private void CancelJob(JobRowViewModel? row) { if (row is null) return; _operations.Cancel(row.Job.Id); } [RelayCommand] private void ClearFinishedJobs() { _operations.ClearFinished(); RefreshJobs(); } [RelayCommand] private async Task UndoAsync() { var result = await _operations.UndoAsync(); if (!result.Success) ErrorMessage = result.Error; await (ActivePane?.RefreshCommand.ExecuteAsync(null) ?? Task.CompletedTask); } [ObservableProperty] private string? _errorMessage; // ── 设置(改动即时生效) ──────────────────────────────────────────────── [ObservableProperty] private bool _showHiddenFiles; [ObservableProperty] private bool _showSystemFiles; [ObservableProperty] private bool _showFileExtensions; [ObservableProperty] private bool _alwaysShowCheckBoxes; [ObservableProperty] private bool _deleteToRecycleBin; [ObservableProperty] private bool _animationsEnabled; [ObservableProperty] private bool _searchInCurrentFolderOnly; [ObservableProperty] private AppThemeMode _themeMode; partial void OnShowHiddenFilesChanged(bool value) { _settings.ShowHiddenFiles = value; _settings.Save(); _ = ActivePane?.RefreshCommand.ExecuteAsync(null); } partial void OnShowSystemFilesChanged(bool value) { _settings.ShowSystemFiles = value; _settings.Save(); _ = ActivePane?.RefreshCommand.ExecuteAsync(null); } partial void OnShowFileExtensionsChanged(bool value) { _settings.ShowFileExtensions = value; _settings.Save(); foreach (var pane in AllPanes()) pane.RebuildDisplayNames(); } partial void OnAlwaysShowCheckBoxesChanged(bool value) { _settings.AlwaysShowCheckBoxes = value; _settings.Save(); } partial void OnDeleteToRecycleBinChanged(bool value) { _settings.DeleteToRecycleBin = value; _settings.Save(); } partial void OnAnimationsEnabledChanged(bool value) { _settings.AnimationsEnabled = value; _settings.Save(); AnimationsToggled?.Invoke(this, value); } public event EventHandler? AnimationsToggled; public event EventHandler? ThemeModeChanged; partial void OnThemeModeChanged(AppThemeMode value) { _settings.ThemeMode = value; _settings.Save(); ThemeModeChanged?.Invoke(this, value); } partial void OnSearchInCurrentFolderOnlyChanged(bool value) { _settings.SearchScope = value ? SearchScope.CurrentFolder : SearchScope.Global; _settings.Save(); } private IEnumerable AllPanes() { foreach (var tab in Tabs) { yield return tab.Primary; if (tab.Secondary is not null) yield return tab.Secondary; } } public void SaveOpenTabs() { _settings.OpenTabs = Tabs .Select(t => t.Primary.CurrentPath) .Where(p => !string.IsNullOrEmpty(p) && !p.StartsWith("::", StringComparison.Ordinal)) .ToList(); _settings.Save(); } /// 启动:恢复上次的标签页并建立索引。 public async Task InitializeAsync() { if (_settings.RestoreTabsOnStartup && _settings.OpenTabs.Count > 0) { foreach (var path in _settings.OpenTabs.Take(8)) { if (Directory.Exists(path)) AddTab(NavigationLocation.FromPath(path), select: false); } } if (Tabs.Count == 0) AddTab(); SelectedTab = Tabs[0]; if (_settings.DualPane) SelectedTab?.ToggleDualPane(); if (_settings.IndexOnStartup) { await BuildIndexAsync(); } } }