diff --git a/LANCommander/Components/ArchiveBrowserDialog.razor b/LANCommander/Components/ArchiveBrowserDialog.razor deleted file mode 100644 index 79b270b..0000000 --- a/LANCommander/Components/ArchiveBrowserDialog.razor +++ /dev/null @@ -1,14 +0,0 @@ -@inherits FeedbackComponent> -@using System.IO.Compression; -@using LANCommander.Models; - - - -@code { - private IEnumerable SelectedFiles { get; set; } - - public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args) - { - await base.OkCancelRefWithResult!.OnOk(SelectedFiles); - } -} diff --git a/LANCommander/Components/FileManagerComponents/FileManager.razor b/LANCommander/Components/FileManagerComponents/FileManager.razor new file mode 100644 index 0000000..b7d91ae --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/FileManager.razor @@ -0,0 +1,509 @@ +@using AntDesign.TableModels; +@using LANCommander.Components.FileManagerComponents +@inject ArchiveService ArchiveService +@inject IMessageService MessageService +@namespace LANCommander.Components + +
+ + + @if (Features.HasFlag(FileManagerFeatures.NavigationBack)) + { + + +
+ + + + + @code { + [Parameter] public Guid ArchiveId { get; set; } + [Parameter] public string WorkingDirectory { get; set; } + [Parameter] public bool SelectMultiple { get; set; } = true; + [Parameter] public FileManagerFeatures Features { get; set; } = FileManagerFeatures.NavigationBack | FileManagerFeatures.NavigationForward | FileManagerFeatures.UpALevel | FileManagerFeatures.Refresh | FileManagerFeatures.Breadcrumbs | FileManagerFeatures.NewFolder | FileManagerFeatures.UploadFile | FileManagerFeatures.Delete; + [Parameter] public IEnumerable Selected { get; set; } = new List(); + [Parameter] public EventCallback> SelectedChanged { get; set; } + [Parameter] public Func EntrySelectable { get; set; } = _ => true; + [Parameter] public Func EntryVisible { get; set; } = _ => true; + + FileManagerSource Source = FileManagerSource.FileSystem; + + FileManagerDirectory Path { get; set; } = new FileManagerDirectory(); + + List Past { get; set; } = new List(); + List Future { get; set; } = new List(); + List Breadcrumbs = new List(); + + List Entries { get; set; } = new List(); + HashSet Directories { get; set; } = new HashSet(); + + NewFolderModal NewFolderModal; + UploadModal UploadModal; + + Dictionary OnRow(RowData row) => new() + { + ["data-path"] = row.Data.Path, + ["ondblclick"] = ((System.Action)delegate + { + if (row.Data is FileManagerDirectory) + ChangeDirectory((FileManagerDirectory)row.Data, true); + }) + }; + + protected override async Task OnInitializedAsync() + { + if (!String.IsNullOrWhiteSpace(WorkingDirectory)) + Source = FileManagerSource.FileSystem; + else if (ArchiveId != Guid.Empty) + Source = FileManagerSource.Archive; + + Directories = await GetDirectoriesAsync(); + } + + async Task> GetDirectoriesAsync() + { + switch (Source) + { + case FileManagerSource.FileSystem: + return await GetFileSystemDirectoriesAsync(WorkingDirectory); + case FileManagerSource.Archive: + return await GetArchiveDirectoriesAsync(ArchiveId); + } + + return new HashSet(); + } + + async Task> GetFileSystemDirectoriesAsync(string path) + { + var paths = Directory.EnumerateDirectories(path, "*", new EnumerationOptions + { + IgnoreInaccessible = true, + RecurseSubdirectories = true, + MaxRecursionDepth = 1 + }); + + var root = new FileManagerDirectory + { + Name = path, + Path = path, + IsExpanded = true + }; + + root.PopulateChildren(paths); + + await ChangeDirectory(root, true); + + return new HashSet + { + root + }; + } + + async Task> GetArchiveDirectoriesAsync(Guid archiveId) + { + var entries = await ArchiveService.GetContents(archiveId); + var directories = new HashSet(); + + var root = new FileManagerDirectory + { + Name = "Root", + Path = "", + IsExpanded = true + }; + + root.PopulateChildren(entries); + + await ChangeDirectory(root, true); + + return new HashSet + { + root + }; + } + + string GetEntryName(IFileManagerEntry entry) + { + if (String.IsNullOrWhiteSpace(entry.Name) && entry.Size == 0) + { + return entry.Path.TrimEnd('/').Split('/').Last(); + } + else + return entry.Name; + } + + async Task ChangeDirectory(FileManagerDirectory directory, bool clearFuture) + { + if (Path != null && !String.IsNullOrWhiteSpace(Path.Path) && directory.Path != Path.Path && Past.LastOrDefault()?.Path != directory.Path) + Past.Add(Path); + + Path = directory; + + await UpdateEntries(); + UpdateBreadcrumbs(); + + if (clearFuture) + Future.Clear(); + + StateHasChanged(); + } + + async Task ExpandTree(TreeEventArgs args) + { + if (Source == FileManagerSource.FileSystem) + { + var directory = (FileManagerDirectory)args.Node.DataItem; + + foreach (var child in directory.Children) + { + await Task.Run(() => + { + var paths = Directory.EnumerateDirectories(child.Path, "*", new EnumerationOptions + { + IgnoreInaccessible = true, + RecurseSubdirectories = true, + MaxRecursionDepth = 1 + }); + + child.PopulateChildren(paths); + }); + } + } + } + + async Task UpdateEntries() + { + Entries.Clear(); + + switch (Source) + { + case FileManagerSource.FileSystem: + await Task.Run(UpdateFileSystemEntries); + break; + + case FileManagerSource.Archive: + await UpdateArchiveEntries(); + break; + } + } + + void UpdateFileSystemEntries() + { + var entries = Directory.EnumerateFileSystemEntries(Path.Path); + var separator = System.IO.Path.DirectorySeparatorChar; + + foreach (var entry in entries) + { + if (Directory.Exists(entry)) + { + try + { + var info = new DirectoryInfo(entry); + var directory = new FileManagerDirectory + { + Path = entry, + Name = entry.Substring(Path.Path.Length).TrimStart(separator), + ModifiedOn = info.LastWriteTime, + CreatedOn = info.CreationTime, + Parent = Path + }; + + if (EntryVisible.Invoke(directory)) + Entries.Add(directory); + } + catch { } + } + else + { + try + { + var info = new FileInfo(entry); + var file = new FileManagerFile + { + Path = entry, + Name = System.IO.Path.GetFileName(entry), + ModifiedOn = info.LastWriteTime, + CreatedOn = info.CreationTime, + Size = info.Length, + Parent = Path + }; + + if (EntryVisible.Invoke(file)) + Entries.Add(file); + } + catch { } + } + } + } + + async Task UpdateArchiveEntries() + { + var entries = await ArchiveService.GetContents(ArchiveId); + var separator = '/'; + + foreach (var entry in entries.Where(e => e.FullName != Path.Path && e.FullName.StartsWith(Path.Path) && !e.FullName.Substring(Path.Path.Length).TrimEnd(separator).Contains(separator))) + { + if (entry.FullName.EndsWith(separator)) + { + var directory = new FileManagerDirectory + { + Path = entry.FullName, + Name = entry.Name, + ModifiedOn = entry.LastWriteTime.UtcDateTime.ToLocalTime(), + CreatedOn = entry.LastWriteTime.UtcDateTime.ToLocalTime(), + Size = entry.Length, + Parent = Path + }; + + if (EntryVisible.Invoke(directory)) + Entries.Add(directory); + } + else + { + var file = new FileManagerFile + { + Path = entry.FullName, + Name = entry.Name, + ModifiedOn = entry.LastWriteTime.UtcDateTime.ToLocalTime(), + CreatedOn = entry.LastWriteTime.UtcDateTime.ToLocalTime(), + Size = entry.Length, + Parent = Path + }; + + if (EntryVisible.Invoke(file)) + Entries.Add(file); + } + } + } + + void UpdateBreadcrumbs() + { + Breadcrumbs.Clear(); + + var currentPath = Path; + + while (currentPath != null) + { + Breadcrumbs.Add(currentPath); + + currentPath = currentPath.Parent; + } + + Breadcrumbs.Reverse(); + } + + async Task NavigateBack() + { + if (Past.Count > 0) + { + Future.Add(Path); + await ChangeDirectory(Past.Last(), false); + Past = Past.Take(Past.Count - 1).ToList(); + } + } + + async Task NavigateForward() + { + if (Future.Count > 0) + { + Past.Add(Path); + await ChangeDirectory(Future.First(), false); + Future = Future.Skip(1).ToList(); + } + } + + async Task NavigateUp() + { + if (Path.Parent != null) + await ChangeDirectory(Path.Parent, true); + } + + async Task Refresh() + { + await ChangeDirectory(Path, false); + + StateHasChanged(); + } + + async Task AddFolder(string name) + { + if (Source == FileManagerSource.Archive) + throw new NotImplementedException(); + + try + { + Directory.CreateDirectory(System.IO.Path.Combine(Path.Path, name)); + + await Refresh(); + + await MessageService.Success("Folder created!"); + } + catch + { + await MessageService.Error("Error creating folder!"); + } + } + + async Task Delete() + { + if (Source == FileManagerSource.Archive) + throw new NotImplementedException(); + + try + { + foreach (var entry in Selected) + { + if (entry is FileManagerDirectory) + Directory.Delete(entry.Path); + else if (entry is FileManagerFile) + File.Delete(entry.Path); + } + + Selected = new List(); + MessageService.Success("Deleted!"); + } + catch + { + MessageService.Error("Error deleting!"); + } + + await Refresh(); + } +} diff --git a/LANCommander/Components/FileManagerComponents/FileManagerDirectory.cs b/LANCommander/Components/FileManagerComponents/FileManagerDirectory.cs new file mode 100644 index 0000000..fb8cef2 --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/FileManagerDirectory.cs @@ -0,0 +1,56 @@ +using System.IO.Compression; + +namespace LANCommander.Components.FileManagerComponents +{ + public class FileManagerDirectory : FileManagerEntry + { + public bool IsExpanded { get; set; } = false; + public bool HasChildren => Children != null && Children.Count > 0; + public HashSet Children { get; set; } = new HashSet(); + + public void PopulateChildren(IEnumerable entries) + { + var path = Path == "/" ? "" : Path; + var childPaths = entries.Where(e => e.FullName.EndsWith('/')); + var directChildren = childPaths.Where(p => p.FullName != path && p.FullName.StartsWith(path) && p.FullName.Substring(path.Length).TrimEnd('/').Split('/').Length == 1); + + foreach (var directChild in directChildren) + { + var child = new FileManagerDirectory() + { + Path = directChild.FullName, + Name = directChild.FullName.Substring(path.Length).TrimEnd('/'), + Parent = this + }; + + child.PopulateChildren(entries); + + Children.Add(child); + } + } + + public void PopulateChildren(IEnumerable entries) + { + var separator = System.IO.Path.DirectorySeparatorChar; + var childPaths = entries.Where(e => e.StartsWith(Path)); + var directChildren = childPaths.Where(p => p != Path && p.Substring(Path.Length + 1).Split(separator).Length == 1); + + foreach (var directChild in directChildren) + { + if (!Children.Any(c => c.Path == directChild)) + { + var child = new FileManagerDirectory() + { + Path = directChild, + Name = directChild.Substring(Path.Length).TrimStart(separator), + Parent = this + }; + + child.PopulateChildren(entries); + + Children.Add(child); + } + } + } + } +} diff --git a/LANCommander/Components/FileManagerComponents/FileManagerEntry.cs b/LANCommander/Components/FileManagerComponents/FileManagerEntry.cs new file mode 100644 index 0000000..e47c455 --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/FileManagerEntry.cs @@ -0,0 +1,12 @@ +namespace LANCommander.Components.FileManagerComponents +{ + public abstract class FileManagerEntry : IFileManagerEntry + { + public string Name { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public FileManagerDirectory Parent { get; set; } + public DateTime ModifiedOn { get; set; } + public DateTime CreatedOn { get; set; } + } +} diff --git a/LANCommander/Components/FileManagerComponents/FileManagerFeatures.cs b/LANCommander/Components/FileManagerComponents/FileManagerFeatures.cs new file mode 100644 index 0000000..baaf7af --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/FileManagerFeatures.cs @@ -0,0 +1,15 @@ +namespace LANCommander.Components.FileManagerComponents +{ + [Flags] + public enum FileManagerFeatures + { + NavigationBack = 0, + NavigationForward = 1, + UpALevel = 2, + Refresh = 4, + Breadcrumbs = 8, + NewFolder = 16, + UploadFile = 32, + Delete = 64, + } +} diff --git a/LANCommander/Components/FileManagerComponents/FileManagerFile.cs b/LANCommander/Components/FileManagerComponents/FileManagerFile.cs new file mode 100644 index 0000000..5ebc829 --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/FileManagerFile.cs @@ -0,0 +1,76 @@ +namespace LANCommander.Components.FileManagerComponents +{ + public class FileManagerFile : FileManagerEntry + { + public string Extension => Name.Contains('.') ? Name.Split('.').Last() : Name; + + public string GetIcon() + { + switch (Extension) + { + case "": + return "folder"; + + case "exe": + return "code"; + + case "zip": + case "rar": + case "7z": + case "gz": + case "tar": + return "file-zip"; + + case "wad": + case "pk3": + case "pak": + case "cab": + return "file-zip"; + + case "txt": + case "cfg": + case "config": + case "ini": + case "yml": + case "yaml": + case "log": + case "doc": + case "nfo": + return "file-text"; + + case "bat": + case "ps1": + case "json": + return "code"; + + case "bik": + case "avi": + case "mov": + case "mp4": + case "m4v": + case "mkv": + case "wmv": + case "mpg": + case "mpeg": + case "flv": + return "video-camera"; + + case "dll": + return "api"; + + case "hlp": + return "file-unknown"; + + case "png": + case "bmp": + case "jpeg": + case "jpg": + case "gif": + return "file-image"; + + default: + return "file"; + } + } + } +} diff --git a/LANCommander/Components/FileManagerComponents/FileManagerSource.cs b/LANCommander/Components/FileManagerComponents/FileManagerSource.cs new file mode 100644 index 0000000..8382a5a --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/FileManagerSource.cs @@ -0,0 +1,8 @@ +namespace LANCommander.Components.FileManagerComponents +{ + public enum FileManagerSource + { + FileSystem, + Archive + } +} diff --git a/LANCommander/Components/FileManagerComponents/IFileManagerEntry.cs b/LANCommander/Components/FileManagerComponents/IFileManagerEntry.cs new file mode 100644 index 0000000..111e9fd --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/IFileManagerEntry.cs @@ -0,0 +1,12 @@ +namespace LANCommander.Components.FileManagerComponents +{ + public interface IFileManagerEntry + { + public string Name { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public FileManagerDirectory Parent { get; set; } + public DateTime ModifiedOn { get; set; } + public DateTime CreatedOn { get; set; } + } +} diff --git a/LANCommander/Components/FileManagerComponents/NewFolderModal.razor b/LANCommander/Components/FileManagerComponents/NewFolderModal.razor new file mode 100644 index 0000000..5df4468 --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/NewFolderModal.razor @@ -0,0 +1,34 @@ + + + + +@code { + [Parameter] public EventCallback OnFolderNameEntered { get; set; } + + bool Visible { get; set; } = false; + string Name { get; set; } + + protected override async Task OnInitializedAsync() + { + Name = ""; + } + + public void Open() + { + Name = ""; + Visible = true; + } + + public void Close() + { + Visible = false; + } + + async Task OnOk(MouseEventArgs e) + { + if (OnFolderNameEntered.HasDelegate) + await OnFolderNameEntered.InvokeAsync(Name); + + Close(); + } +} diff --git a/LANCommander/Components/FileManagerComponents/UploadModal.razor b/LANCommander/Components/FileManagerComponents/UploadModal.razor new file mode 100644 index 0000000..4c2cdc2 --- /dev/null +++ b/LANCommander/Components/FileManagerComponents/UploadModal.razor @@ -0,0 +1,42 @@ + + +

+ +

+

Click or Drag Files

+
+
+ + @code { + [Parameter] public string Path { get; set; } + [Parameter] public EventCallback OnUploadCompleted { get; set; } + + bool Visible = false; + + Dictionary Data = new Dictionary(); + + protected override void OnParametersSet() + { + Data["Path"] = Path; + } + + public void Open() + { + Visible = true; + StateHasChanged(); + } + + public void Close() + { + Visible = false; + StateHasChanged(); + } + + async Task OnCompleted() + { + Close(); + + if (OnUploadCompleted.HasDelegate) + await OnUploadCompleted.InvokeAsync(); + } +} diff --git a/LANCommander/Components/FilePicker.razor b/LANCommander/Components/FilePicker.razor new file mode 100644 index 0000000..0c8822a --- /dev/null +++ b/LANCommander/Components/FilePicker.razor @@ -0,0 +1,81 @@ +@using LANCommander.Components.FileManagerComponents; +@using LANCommander.Models; +@using System.IO.Compression; +@inject ModalService ModalService +@inject ArchiveService ArchiveService + + + + + + @if (ArchiveId != Guid.Empty) { + +