using System.Windows; using System.Windows.Controls; using Microsoft.SharePoint.Client; using SharepointToolbox.Core.Helpers; namespace SharepointToolbox.Views.Dialogs; public partial class FolderBrowserDialog : Window { private readonly ClientContext _ctx; private readonly bool _allowFileSelection; private readonly bool _allowFolderCreation; public string SelectedLibrary { get; private set; } = string.Empty; public string SelectedFolderPath { get; private set; } = string.Empty; /// /// Library-relative file paths checked by the user. Only populated when /// was true. Empty if the user picked /// a folder node instead. /// public IReadOnlyList SelectedFilePaths { get; private set; } = Array.Empty(); private readonly List _fileCheckboxes = new(); /// /// Dialog for browsing library folders. Set /// to show individual files (with sizes) and allow ticking them for targeted /// transfer. Set to expose a "New /// Folder" button that creates a folder under the selected node. /// public FolderBrowserDialog(ClientContext ctx, bool allowFileSelection = false, bool allowFolderCreation = false) { InitializeComponent(); _ctx = ctx; _allowFileSelection = allowFileSelection; _allowFolderCreation = allowFolderCreation; if (allowFolderCreation) ActionBar.Visibility = Visibility.Visible; Loaded += OnLoaded; } private async void OnLoaded(object sender, RoutedEventArgs e) { try { var web = _ctx.Web; var lists = _ctx.LoadQuery(web.Lists .Include(l => l.Title, l => l.Hidden, l => l.BaseType, l => l.RootFolder.ServerRelativeUrl) .Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary)); var progress = new Progress(); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None); foreach (var list in lists) { var rootUrl = list.RootFolder.ServerRelativeUrl.TrimEnd('/'); var libNode = MakeFolderNode(list.Title, new FolderNodeInfo(list.Title, string.Empty, rootUrl)); FolderTree.Items.Add(libNode); } StatusText.Text = $"{FolderTree.Items.Count} libraries loaded."; } catch (Exception ex) { StatusText.Text = $"Error: {ex.Message}"; } } private TreeViewItem MakeFolderNode(string name, FolderNodeInfo info) { var node = new TreeViewItem { Header = name, Tag = info, }; // Placeholder child so the expand arrow appears. node.Items.Add(new TreeViewItem { Header = "Loading..." }); node.Expanded += FolderNode_Expanded; return node; } private async void FolderNode_Expanded(object sender, RoutedEventArgs e) { if (sender is not TreeViewItem node || node.Tag is not FolderNodeInfo info) return; // Only load children once. if (!(node.Items.Count == 1 && node.Items[0] is TreeViewItem dummy && dummy.Header?.ToString() == "Loading...")) return; node.Items.Clear(); try { var folder = _ctx.Web.GetFolderByServerRelativeUrl(info.ServerRelativeUrl); _ctx.Load(folder, f => f.StorageMetrics.TotalSize, f => f.StorageMetrics.TotalFileCount, f => f.Folders.Include(sf => sf.Name, sf => sf.ServerRelativeUrl, sf => sf.StorageMetrics.TotalSize, sf => sf.StorageMetrics.TotalFileCount), f => f.Files.Include(fi => fi.Name, fi => fi.Length, fi => fi.ServerRelativeUrl)); var progress = new Progress(); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None); // Annotate the parent node header with total metrics now that they loaded. node.Header = FormatFolderHeader(info.LibraryTitle == info.RelativePath || string.IsNullOrEmpty(info.RelativePath) ? (string)node.Header! : System.IO.Path.GetFileName(info.RelativePath), folder.StorageMetrics.TotalFileCount, folder.StorageMetrics.TotalSize); // Child folders first foreach (var subFolder in folder.Folders) { if (subFolder.Name.StartsWith("_") || subFolder.Name == "Forms") continue; var childRelative = string.IsNullOrEmpty(info.RelativePath) ? subFolder.Name : $"{info.RelativePath}/{subFolder.Name}"; var childInfo = new FolderNodeInfo( info.LibraryTitle, childRelative, subFolder.ServerRelativeUrl); var childNode = MakeFolderNode( FormatFolderHeader(subFolder.Name, subFolder.StorageMetrics.TotalFileCount, subFolder.StorageMetrics.TotalSize), childInfo); node.Items.Add(childNode); } // Files under this folder — only shown when selection is enabled. if (_allowFileSelection) { foreach (var file in folder.Files) { // Library-relative path for the file (used by the transfer service) var fileRel = string.IsNullOrEmpty(info.RelativePath) ? file.Name : $"{info.RelativePath}/{file.Name}"; var cb = new CheckBox { Content = $"{file.Name} ({FormatSize(file.Length)})", Tag = new FileNodeInfo(info.LibraryTitle, fileRel), Margin = new Thickness(4, 2, 0, 2), }; cb.Checked += FileCheckbox_Toggled; cb.Unchecked += FileCheckbox_Toggled; _fileCheckboxes.Add(cb); var fileItem = new TreeViewItem { Header = cb, Focusable = false }; node.Items.Add(fileItem); } } } catch (Exception ex) { node.Items.Add(new TreeViewItem { Header = $"Error: {ex.Message}" }); } } private static string FormatFolderHeader(string name, long fileCount, long totalBytes) { if (fileCount <= 0) return name; return $"{name} ({fileCount} files, {FormatSize(totalBytes)})"; } private static string FormatSize(long bytes) { if (bytes <= 0) return "0 B"; if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB"; if (bytes >= 1_048_576L) return $"{bytes / 1_048_576.0:F2} MB"; if (bytes >= 1024L) return $"{bytes / 1024.0:F2} KB"; return $"{bytes} B"; } private void FolderTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) { if (e.NewValue is TreeViewItem node && node.Tag is FolderNodeInfo info) { SelectedLibrary = info.LibraryTitle; SelectedFolderPath = info.RelativePath; SelectButton.IsEnabled = true; NewFolderButton.IsEnabled = _allowFolderCreation; } else { // File nodes have CheckBox headers, not FolderNodeInfo tags. NewFolderButton.IsEnabled = false; } } private void FileCheckbox_Toggled(object sender, RoutedEventArgs e) { // Enable "Select" as soon as any file is checked — user can confirm // purely via file selection without also picking a folder node. if (_fileCheckboxes.Any(c => c.IsChecked == true)) SelectButton.IsEnabled = true; } private async void NewFolder_Click(object sender, RoutedEventArgs e) { if (FolderTree.SelectedItem is not TreeViewItem node || node.Tag is not FolderNodeInfo info) return; var dlg = new InputDialog("New folder name:", string.Empty) { Owner = this }; if (dlg.ShowDialog() != true) return; var folderName = dlg.ResponseText.Trim(); if (string.IsNullOrEmpty(folderName)) return; try { var parent = _ctx.Web.GetFolderByServerRelativeUrl(info.ServerRelativeUrl); var created = parent.Folders.Add(folderName); _ctx.Load(created, f => f.ServerRelativeUrl, f => f.Name); var progress = new Progress(); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(_ctx, progress, CancellationToken.None); var childRelative = string.IsNullOrEmpty(info.RelativePath) ? created.Name : $"{info.RelativePath}/{created.Name}"; var childInfo = new FolderNodeInfo( info.LibraryTitle, childRelative, created.ServerRelativeUrl); var childNode = MakeFolderNode(created.Name, childInfo); // Expand the parent so the fresh folder is visible immediately. node.IsExpanded = true; node.Items.Add(childNode); StatusText.Text = $"Created: {created.ServerRelativeUrl}"; } catch (Exception ex) { StatusText.Text = $"Error: {ex.Message}"; } } private void Select_Click(object sender, RoutedEventArgs e) { // Harvest checked files (library-relative paths). SelectedFilePaths = _fileCheckboxes .Where(c => c.IsChecked == true && c.Tag is FileNodeInfo) .Select(c => ((FileNodeInfo)c.Tag!).RelativePath) .ToList(); // If files were picked but no folder node was selected, borrow the // library from the first file so the caller still has a valid target. if (SelectedFilePaths.Count > 0 && string.IsNullOrEmpty(SelectedLibrary)) { var firstTag = (FileNodeInfo)_fileCheckboxes .First(c => c.IsChecked == true && c.Tag is FileNodeInfo).Tag!; SelectedLibrary = firstTag.LibraryTitle; SelectedFolderPath = string.Empty; } DialogResult = true; Close(); } private void Cancel_Click(object sender, RoutedEventArgs e) { DialogResult = false; Close(); } private record FolderNodeInfo(string LibraryTitle, string RelativePath, string ServerRelativeUrl); private record FileNodeInfo(string LibraryTitle, string RelativePath); }