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();
}
}
}