From 8dee03cf43fe8ba9cc194896cf8812b99318d1ff Mon Sep 17 00:00:00 2001 From: lucien Date: Tue, 15 Sep 2026 18:26:43 +0800 Subject: [PATCH 01/10] feat(LauncherView): add drag and drop support to open repository Implemented file drag-and-drop functionality for the launcher window, allowing users to quickly open a target repository by dragging its folder directly into the window, thereby enhancing ease of use. --- src/Views/Launcher.axaml | 4 +++- src/Views/Launcher.axaml.cs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml index 00aa09a228..bfd921a3c2 100644 --- a/src/Views/Launcher.axaml +++ b/src/Views/Launcher.axaml @@ -12,7 +12,9 @@ Icon="/App.ico" Title="{Binding Title}" MinWidth="1024" MinHeight="600"> - + diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index e0708895cc..a9ef8afb0b 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -447,6 +447,39 @@ private async void OnShowNewVersion(object sender, RoutedEventArgs e) e.Handled = true; } + private void DragOverWindow(object sender, DragEventArgs e) + { + if (e.DataTransfer.Contains(DataFormat.File)) + { + e.DragEffects = DragDropEffects.Move; + e.Handled = true; + } + else + { + e.DragEffects = DragDropEffects.None; + e.Handled = true; + } + } + + private void DropOnWindow(object sender, DragEventArgs e) + { + if (DataContext is not ViewModels.Launcher launcher) + return; + + if (e.DataTransfer.Contains(DataFormat.File)) + { + var items = e.DataTransfer.TryGetFiles() ?? []; + foreach (var item in items) + { + var path = item.Path.LocalPath; + if (!string.IsNullOrEmpty(path)) + launcher.TryOpenRepositoryFromPath(path); + } + + e.Handled = true; + } + } + private GridLength _captionHeight = new(32); private WindowState _lastWindowState = WindowState.Normal; } From 063b3a6e6be6a26cdeca4fefb4706b87f5f2aec8 Mon Sep 17 00:00:00 2001 From: lucien Date: Tue, 15 Sep 2026 20:20:13 +0800 Subject: [PATCH 02/10] feat(preferences): Added recent repository list and directory tree browsing functionality Introduced a repository management panel to the welcome page, featuring: 1. Added a `RecentRepo` model to store information about recently accessed repositories. 2. Implemented a directory tree structure that scans the clone directory and displays Git repositories. 3. Added a `RecentRepositories` list to Preferences for persistent storage of recent repository paths. 4. Added multi-language support for the repository management and directory tree interfaces. 5. Implemented interaction logic such as expanding/collapsing tree nodes, context menus, and opening repositories via double-click. 6. Automatically records repositories to the recent list upon opening, retaining up to 20 entries. 7. Supports refreshing the clone directory scan results and filtering out hidden folders and `node_modules` directories. --- src/Resources/Locales/en_US.axaml | 5 + src/Resources/Locales/zh_CN.axaml | 5 + src/ViewModels/DirectoryTree.cs | 242 ++++++++++++++++++++++++++++ src/ViewModels/DirectoryTreeNode.cs | 30 ++++ src/ViewModels/Preferences.cs | 6 + src/ViewModels/RecentRepo.cs | 16 ++ src/ViewModels/RepositoryNode.cs | 1 + src/Views/Welcome.axaml | 170 ++++++++++++++++++- src/Views/Welcome.axaml.cs | 164 +++++++++++++++++++ 9 files changed, 637 insertions(+), 2 deletions(-) create mode 100644 src/ViewModels/DirectoryTree.cs create mode 100644 src/ViewModels/DirectoryTreeNode.cs create mode 100644 src/ViewModels/RecentRepo.cs diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 269de476b2..7821196521 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1018,6 +1018,11 @@ Open Terminal Rescan Repositories in Default Clone Dir Search Repositories... + Repository Manager + Clone Directory + Refresh + Right-click to scan clone directory + Recent Local Changes Git Ignore Ignore all *{0} files diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 8cecd0e571..ca0e90c65c 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1022,6 +1022,11 @@ 打开终端 重新扫描默认克隆路径下的仓库 快速查找仓库... + 仓库管理 + 克隆目录 + 刷新 + 右键点击扫描克隆目录 + 最近 本地更改 添加至 .gitignore 忽略列表 忽略所有 *{0} 文件 diff --git a/src/ViewModels/DirectoryTree.cs b/src/ViewModels/DirectoryTree.cs new file mode 100644 index 0000000000..b46b859a74 --- /dev/null +++ b/src/ViewModels/DirectoryTree.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using Avalonia.Collections; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class DirectoryTree : ObservableObject + { + private static DirectoryTree _instance; + public static DirectoryTree Instance => _instance ??= new DirectoryTree(); + + public AvaloniaList Rows { get; } = []; + + public AvaloniaList RecentRepos { get; } = []; + + private bool _hasRecentRepos; + public bool HasRecentRepos + { + get => _hasRecentRepos; + private set => SetProperty(ref _hasRecentRepos, value); + } + + private bool _isEmpty = true; + public bool IsEmpty + { + get => _isEmpty; + private set => SetProperty(ref _isEmpty, value); + } + + private bool _isScanning; + public bool IsScanning + { + get => _isScanning; + private set => SetProperty(ref _isScanning, value); + } + + private bool _recentLoaded; + + private DirectoryTree() { } + + private void EnsureRecentLoaded() + { + if (_recentLoaded) + return; + _recentLoaded = true; + LoadRecentRepos(); + } + + public void RecordRecentRepo(string path) + { + EnsureRecentLoaded(); + + if (string.IsNullOrEmpty(path) || !Directory.Exists(path)) + return; + + var name = Path.GetFileName(path); + + Dispatcher.UIThread.Post(() => + { + for (int i = RecentRepos.Count - 1; i >= 0; i--) + { + if (RecentRepos[i].Path.Equals(path, StringComparison.Ordinal)) + { + RecentRepos.RemoveAt(i); + break; + } + } + + RecentRepos.Insert(0, new RecentRepo(path, name)); + + while (RecentRepos.Count > 20) + RecentRepos.RemoveAt(RecentRepos.Count - 1); + + UpdateEmptyState(); + SaveRecentRepos(); + }); + } + + public async Task ScanAsync() + { + EnsureRecentLoaded(); + + if (_isScanning) + return; + + var cloneDir = Preferences.Instance.GitDefaultCloneDir; + if (string.IsNullOrEmpty(cloneDir) || !Directory.Exists(cloneDir)) + { + Dispatcher.UIThread.Post(() => + { + Rows.Clear(); + UpdateEmptyState(); + }); + return; + } + + _isScanning = true; + + try + { + var rootDir = new DirectoryInfo(cloneDir); + var rootNode = new DirectoryTreeNode(rootDir.FullName, rootDir.Name, false); + await BuildTreeAsync(rootNode, rootDir, 0); + PruneEmptyFolders(rootNode); + + var rows = new List(); + foreach (var child in rootNode.Children) + MakeTreeRows(rows, child, 0); + + Dispatcher.UIThread.Post(() => + { + Rows.Clear(); + Rows.AddRange(rows); + UpdateEmptyState(); + }); + } + finally + { + _isScanning = false; + } + } + + public void ToggleNodeIsExpanded(DirectoryTreeNode node) + { + node.IsExpanded = !node.IsExpanded; + + var depth = node.Depth; + var idx = Rows.IndexOf(node); + if (idx == -1) + return; + + if (node.IsExpanded) + { + var subrows = new List(); + foreach (var child in node.Children) + MakeTreeRows(subrows, child, depth + 1); + Rows.InsertRange(idx + 1, subrows); + } + else + { + var removeCount = 0; + for (int i = idx + 1; i < Rows.Count; i++) + { + if (Rows[i].Depth <= depth) + break; + removeCount++; + } + Rows.RemoveRange(idx + 1, removeCount); + } + } + + private void UpdateEmptyState() + { + HasRecentRepos = RecentRepos.Count > 0; + IsEmpty = Rows.Count == 0 && !HasRecentRepos; + } + + private void LoadRecentRepos() + { + foreach (var path in Preferences.Instance.RecentRepositories) + { + if (Directory.Exists(path)) + RecentRepos.Add(new RecentRepo(path, Path.GetFileName(path))); + } + UpdateEmptyState(); + } + + private void SaveRecentRepos() + { + Preferences.Instance.RecentRepositories.Clear(); + foreach (var repo in RecentRepos) + Preferences.Instance.RecentRepositories.Add(repo.Path); + Preferences.Instance.Save(); + } + + private void MakeTreeRows(List rows, DirectoryTreeNode node, int depth) + { + node.Depth = depth; + rows.Add(node); + + if (node.IsRepository || !node.IsExpanded) + return; + + foreach (var child in node.Children) + MakeTreeRows(rows, child, depth + 1); + } + + private void PruneEmptyFolders(DirectoryTreeNode node) + { + for (int i = node.Children.Count - 1; i >= 0; i--) + { + var child = node.Children[i]; + if (!child.IsRepository) + { + PruneEmptyFolders(child); + if (child.Children.Count == 0) + node.Children.RemoveAt(i); + } + } + } + + private async Task BuildTreeAsync(DirectoryTreeNode node, DirectoryInfo dir, int depth) + { + if (depth > 5) + return; + + var subdirs = dir.GetDirectories("*", new EnumerationOptions() + { + AttributesToSkip = FileAttributes.Hidden | FileAttributes.System, + IgnoreInaccessible = true, + }); + + foreach (var subdir in subdirs) + { + if (subdir.Name.StartsWith(".", StringComparison.Ordinal) || + subdir.Name.Equals("node_modules", StringComparison.Ordinal)) + continue; + + var gitDir = Path.Combine(subdir.FullName, ".git"); + var isRepo = Directory.Exists(gitDir) || File.Exists(gitDir); + + if (!isRepo) + { + isRepo = await new Commands.IsBareRepository(subdir.FullName).GetResultAsync(); + } + + var childNode = new DirectoryTreeNode(subdir.FullName, subdir.Name, isRepo); + node.Children.Add(childNode); + + if (!isRepo) + { + await BuildTreeAsync(childNode, subdir, depth + 1); + } + } + } + } +} diff --git a/src/ViewModels/DirectoryTreeNode.cs b/src/ViewModels/DirectoryTreeNode.cs new file mode 100644 index 0000000000..4e114f73a0 --- /dev/null +++ b/src/ViewModels/DirectoryTreeNode.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class DirectoryTreeNode : ObservableObject + { + public string Path { get; set; } + public string Name { get; set; } + public bool IsRepository { get; set; } + + public int Depth { get; set; } + + private bool _isExpanded; + public bool IsExpanded + { + get => _isExpanded; + set => SetProperty(ref _isExpanded, value); + } + + public List Children { get; } = []; + + public DirectoryTreeNode(string path, string name, bool isRepository) + { + Path = path; + Name = name; + IsRepository = isRepository; + } + } +} diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 58ac55503b..5960b39f27 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -468,6 +468,12 @@ public List RepositoryNodes set; } = []; + public List RecentRepositories + { + get; + set; + } = []; + public List Workspaces { get; diff --git a/src/ViewModels/RecentRepo.cs b/src/ViewModels/RecentRepo.cs new file mode 100644 index 0000000000..0ee0ecd710 --- /dev/null +++ b/src/ViewModels/RecentRepo.cs @@ -0,0 +1,16 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class RecentRepo : ObservableObject + { + public string Path { get; set; } + public string Name { get; set; } + + public RecentRepo(string path, string name) + { + Path = path; + Name = name; + } + } +} diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index 9d7ab9debc..847896901f 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -99,6 +99,7 @@ public void Open() } else if (Directory.Exists(_id)) { + DirectoryTree.Instance.RecordRecentRepo(_id); App.GetLauncher().OpenRepositoryInTab(this, null); } } diff --git a/src/Views/Welcome.axaml b/src/Views/Welcome.axaml index c777852982..b715457b98 100644 --- a/src/Views/Welcome.axaml +++ b/src/Views/Welcome.axaml @@ -11,13 +11,179 @@ x:DataType="vm:Welcome"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index c93c27ebb8..dda3970bf4 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -25,6 +25,20 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) } } + public class DirectoryTreeNodeToggleButton : ToggleButton + { + protected override Type StyleKeyOverride => typeof(ToggleButton); + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + DataContext is ViewModels.DirectoryTreeNode { IsRepository: false } node) + ViewModels.DirectoryTree.Instance.ToggleNodeIsExpanded(node); + + e.Handled = true; + } + } + public class RepositoryListBox : ListBoxEx { protected override Type StyleKeyOverride => typeof(ListBox); @@ -423,6 +437,156 @@ private void OnDoubleTappedTreeNode(object sender, TappedEventArgs e) } } + private void OnRefreshDirectoryTree(object sender, RoutedEventArgs e) + { + _ = ViewModels.DirectoryTree.Instance.ScanAsync(); + } + + private void OnDirectoryTreeContextRequested(object sender, ContextRequestedEventArgs e) + { + // Only show the panel-level context menu if the click was not on a node + if (sender is not Grid || (sender as Grid).DataContext is not ViewModels.DirectoryTreeNode) + { + var menu = new ContextMenu(); + + var scan = new MenuItem(); + scan.Header = App.Text("Welcome.DirectoryTree.Refresh"); + scan.Icon = this.CreateMenuIcon("Icons.Scan"); + scan.Click += (_, ev) => + { + _ = ViewModels.DirectoryTree.Instance.ScanAsync(); + ev.Handled = true; + }; + + menu.Items.Add(scan); + menu.Open(sender as Control); + } + + e.Handled = true; + } + + private void OnDirectoryTreeNodeContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is not Grid { DataContext: ViewModels.DirectoryTreeNode node } grid) + return; + + var menu = new ContextMenu(); + + if (node.IsRepository) + { + var open = new MenuItem(); + open.Header = App.Text("Welcome.OpenOrInit"); + open.Icon = this.CreateMenuIcon("Icons.Folder.Open"); + open.Click += (_, ev) => + { + var launcher = App.GetLauncher(); + if (launcher != null) + launcher.TryOpenRepositoryFromPath(node.Path); + ev.Handled = true; + }; + + menu.Items.Add(open); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + + var explore = new MenuItem(); + explore.Header = App.Text("Repository.Explore"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(node.Path); + ev.Handled = true; + }; + + var terminal = new MenuItem(); + terminal.Header = App.Text("Repository.Terminal"); + terminal.Icon = this.CreateMenuIcon("Icons.Terminal"); + terminal.Click += (_, ev) => + { + Native.OS.OpenTerminal(node.Path); + ev.Handled = true; + }; + + menu.Items.Add(explore); + menu.Items.Add(terminal); + menu.Open(grid); + e.Handled = true; + } + + private void OnDoubleTappedDirectoryTreeNode(object sender, TappedEventArgs e) + { + if (sender is Grid { DataContext: ViewModels.DirectoryTreeNode node }) + { + if (node.IsRepository) + { + var launcher = App.GetLauncher(); + if (launcher != null) + launcher.TryOpenRepositoryFromPath(node.Path); + } + else + { + ViewModels.DirectoryTree.Instance.ToggleNodeIsExpanded(node); + } + + e.Handled = true; + } + } + + private void OnRecentRepoContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is not Grid { DataContext: ViewModels.RecentRepo repo } grid) + return; + + var menu = new ContextMenu(); + + var open = new MenuItem(); + open.Header = App.Text("Welcome.OpenOrInit"); + open.Icon = this.CreateMenuIcon("Icons.Folder.Open"); + open.Click += (_, ev) => + { + var launcher = App.GetLauncher(); + if (launcher != null) + launcher.TryOpenRepositoryFromPath(repo.Path); + ev.Handled = true; + }; + + var explore = new MenuItem(); + explore.Header = App.Text("Repository.Explore"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(repo.Path); + ev.Handled = true; + }; + + var terminal = new MenuItem(); + terminal.Header = App.Text("Repository.Terminal"); + terminal.Icon = this.CreateMenuIcon("Icons.Terminal"); + terminal.Click += (_, ev) => + { + Native.OS.OpenTerminal(repo.Path); + ev.Handled = true; + }; + + menu.Items.Add(open); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(explore); + menu.Items.Add(terminal); + menu.Open(grid); + e.Handled = true; + } + + private void OnDoubleTappedRecentRepo(object sender, TappedEventArgs e) + { + if (sender is Grid { DataContext: ViewModels.RecentRepo repo }) + { + var launcher = App.GetLauncher(); + if (launcher != null) + launcher.TryOpenRepositoryFromPath(repo.Path); + e.Handled = true; + } + } + private PointerPressedEventArgs _pressTreeNodeEvent = null; private bool _startDragTreeNode = false; private readonly DataFormat _dndRepoNode = DataFormat.CreateStringApplicationFormat("sourcegit-dnd-repo-node"); From c221964bb4fef37324d60fbf18cfbd37177d40f0 Mon Sep 17 00:00:00 2001 From: lucien Date: Tue, 15 Sep 2026 20:29:42 +0800 Subject: [PATCH 03/10] feat(DirectoryTree): Added error prompt dialog for directory scanning and corresponding i18n support 1. Added localization strings (zh_CN and en_US) for scan error prompts and button labels (Continue/Stop/Open Path). 2. Created the `ScanError` dialog view and logic class to handle error notifications and the Pause/Resume/Open Directory functions. 3. Optimized directory scanning logic: exceptions are caught and an error dialog is displayed, allowing users to choose whether to continue scanning, stop scanning, or open the path where the error occurred. 4. Added a scan-stop status flag to enable early termination of the scanning process. --- src/Resources/Locales/en_US.axaml | 4 ++ src/Resources/Locales/zh_CN.axaml | 4 ++ src/ViewModels/DirectoryTree.cs | 81 ++++++++++++++++++++++++------- src/Views/ScanError.axaml | 76 +++++++++++++++++++++++++++++ src/Views/ScanError.axaml.cs | 47 ++++++++++++++++++ 5 files changed, 194 insertions(+), 18 deletions(-) create mode 100644 src/Views/ScanError.axaml create mode 100644 src/Views/ScanError.axaml.cs diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 7821196521..a2e1406bd5 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1022,6 +1022,10 @@ Clone Directory Refresh Right-click to scan clone directory + Error scanning directory "{0}": {1} Continue scanning? + Continue + Stop + Open Path Recent Local Changes Git Ignore diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index ca0e90c65c..8cc2c4f3f0 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1026,6 +1026,10 @@ 克隆目录 刷新 右键点击扫描克隆目录 + 扫描目录 "{0}" 时出错: {1} 是否继续扫描? + 继续 + 停止 + 打开路径 最近 本地更改 添加至 .gitignore 忽略列表 diff --git a/src/ViewModels/DirectoryTree.cs b/src/ViewModels/DirectoryTree.cs index b46b859a74..8f017eda73 100644 --- a/src/ViewModels/DirectoryTree.cs +++ b/src/ViewModels/DirectoryTree.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Avalonia.Collections; @@ -40,6 +41,7 @@ public bool IsScanning } private bool _recentLoaded; + private bool _stopRequested; private DirectoryTree() { } @@ -100,6 +102,7 @@ public async Task ScanAsync() } _isScanning = true; + _stopRequested = false; try { @@ -119,6 +122,10 @@ public async Task ScanAsync() UpdateEmptyState(); }); } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[DirectoryTree] ScanAsync failed: {ex}"); + } finally { _isScanning = false; @@ -206,37 +213,75 @@ private void PruneEmptyFolders(DirectoryTreeNode node) private async Task BuildTreeAsync(DirectoryTreeNode node, DirectoryInfo dir, int depth) { - if (depth > 5) + if (depth > 5 || _stopRequested) return; - var subdirs = dir.GetDirectories("*", new EnumerationOptions() + string[] subdirs; + try { - AttributesToSkip = FileAttributes.Hidden | FileAttributes.System, - IgnoreInaccessible = true, - }); + subdirs = dir.GetDirectories("*", new EnumerationOptions() + { + AttributesToSkip = FileAttributes.Hidden | FileAttributes.System, + IgnoreInaccessible = true, + }).Select(d => d.FullName).ToArray(); + } + catch (Exception ex) + { + var result = await AskScanErrorAsync(dir.FullName, ex.Message); + if (result == Views.ScanErrorResult.Stop) + _stopRequested = true; + return; + } - foreach (var subdir in subdirs) + foreach (var subdirPath in subdirs) { - if (subdir.Name.StartsWith(".", StringComparison.Ordinal) || - subdir.Name.Equals("node_modules", StringComparison.Ordinal)) - continue; + if (_stopRequested) + break; - var gitDir = Path.Combine(subdir.FullName, ".git"); - var isRepo = Directory.Exists(gitDir) || File.Exists(gitDir); + var subdirName = Path.GetFileName(subdirPath); + if (subdirName.StartsWith(".", StringComparison.Ordinal) || + subdirName.Equals("node_modules", StringComparison.Ordinal)) + continue; - if (!isRepo) + try { - isRepo = await new Commands.IsBareRepository(subdir.FullName).GetResultAsync(); - } + var gitDir = Path.Combine(subdirPath, ".git"); + var isRepo = Directory.Exists(gitDir) || File.Exists(gitDir); + + if (!isRepo) + { + isRepo = await new Commands.IsBareRepository(subdirPath).GetResultAsync(); + } - var childNode = new DirectoryTreeNode(subdir.FullName, subdir.Name, isRepo); - node.Children.Add(childNode); + var childNode = new DirectoryTreeNode(subdirPath, subdirName, isRepo); + node.Children.Add(childNode); - if (!isRepo) + if (!isRepo) + { + await BuildTreeAsync(childNode, new DirectoryInfo(subdirPath), depth + 1); + } + } + catch (Exception ex) { - await BuildTreeAsync(childNode, subdir, depth + 1); + var result = await AskScanErrorAsync(subdirPath, ex.Message); + if (result == Views.ScanErrorResult.Stop) + { + _stopRequested = true; + break; + } } } } + + private async Task AskScanErrorAsync(string path, string errorMessage) + { + return await Dispatcher.UIThread.InvokeAsync(() => + { + var dialog = new Views.ScanError(); + dialog.SetData(path, errorMessage); + return dialog.ShowDialog( + (App.Current?.ApplicationLifetime as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime)?.MainWindow); + }); + } } } diff --git a/src/Views/ScanError.axaml b/src/Views/ScanError.axaml new file mode 100644 index 0000000000..ea8246d26c --- /dev/null +++ b/src/Views/ScanError.axaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + +