Reimplemented the file manager with a cleaner ui, a base path protection from the core and modular and expandable
This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
{
|
||||
[Parameter] public FileEntry File { get; set; }
|
||||
|
||||
[Parameter] public IFileAccess FileAccess { get; set; }
|
||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||
|
||||
[Parameter] public bool CloseOnSave { get; set; } = false;
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public IFileAccess FileAccess { get; set; }
|
||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||
|
||||
// Navigation
|
||||
private string Path = "/";
|
||||
@@ -134,7 +134,7 @@
|
||||
// Move
|
||||
private FileEntry MoveEntry;
|
||||
private SmartModal MoveModal;
|
||||
private IFileAccess MoveAccess;
|
||||
private BaseFileAccess MoveAccess;
|
||||
|
||||
private async Task OnPathChanged(string path)
|
||||
{
|
||||
@@ -267,7 +267,7 @@
|
||||
MoveAccess.Dispose();
|
||||
|
||||
// Perform move and process ui updates
|
||||
await FileAccess.Move(MoveEntry.Name, pathToMove + MoveEntry.Name);
|
||||
await FileAccess.Move(MoveEntry, pathToMove + MoveEntry.Name);
|
||||
|
||||
await MoveModal.Hide();
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public IFileAccess FileAccess { get; set; }
|
||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||
|
||||
private CancellationTokenSource Cancellation = new();
|
||||
private string DropzoneId;
|
||||
@@ -65,12 +65,12 @@
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await SharedFileAccessService.Register(FileAccess);
|
||||
//await SharedFileAccessService.Register(FileAccess);
|
||||
|
||||
var token = await SharedFileAccessService.GenerateToken(FileAccess);
|
||||
var url = $"/api/upload?token={token}";
|
||||
//var token = await SharedFileAccessService.GenerateToken(FileAccess);
|
||||
//var url = $"/api/upload?token={token}";
|
||||
|
||||
await DropzoneService.Create(DropzoneId, url);
|
||||
//await DropzoneService.Create(DropzoneId, url);
|
||||
|
||||
Task.Run(async () => // Update the dropzone url every 5 minutes so the token does not expire
|
||||
{
|
||||
@@ -78,9 +78,9 @@
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(5));
|
||||
|
||||
var newToken = await SharedFileAccessService.GenerateToken(FileAccess);
|
||||
var newUrl = $"/api/upload?token={newToken}";
|
||||
await DropzoneService.UpdateUrl(DropzoneId, newUrl);
|
||||
//var newToken = await SharedFileAccessService.GenerateToken(FileAccess);
|
||||
//var newUrl = $"/api/upload?token={newToken}";
|
||||
//await DropzoneService.UpdateUrl(DropzoneId, newUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -89,6 +89,6 @@
|
||||
public async void Dispose()
|
||||
{
|
||||
Cancellation.Cancel();
|
||||
await SharedFileAccessService.Unregister(FileAccess);
|
||||
//await SharedFileAccessService.Unregister(FileAccess);
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public IFileAccess FileAccess { get; set; }
|
||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||
|
||||
[Parameter] public Func<FileEntry, bool>? Filter { get; set; }
|
||||
[Parameter] public bool ShowSize { get; set; } = true;
|
||||
@@ -297,7 +297,7 @@
|
||||
{
|
||||
await ToastService.ModifyProgress(toastId, $"[{i + 1}/{entries.Length}] Deleting '{entry.Name}'");
|
||||
|
||||
await FileAccess.Delete(entry.Name);
|
||||
await FileAccess.Delete(entry);
|
||||
|
||||
i++;
|
||||
}
|
||||
@@ -315,7 +315,7 @@
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return;
|
||||
|
||||
await FileAccess.Move(fileEntry.Name, await FileAccess.GetCurrentDirectory() + name);
|
||||
await FileAccess.Move(fileEntry, await FileAccess.GetCurrentDirectory() + name);
|
||||
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
@@ -332,12 +332,12 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
await SharedFileAccessService.Register(FileAccess);
|
||||
var token = await SharedFileAccessService.GenerateToken(FileAccess);
|
||||
var url = $"/api/download?token={token}&name={fileEntry.Name}";
|
||||
//await SharedFileAccessService.Register(FileAccess);
|
||||
//var token = await SharedFileAccessService.GenerateToken(FileAccess);
|
||||
//var url = $"/api/download?token={token}&name={fileEntry.Name}";
|
||||
|
||||
await ToastService.Info("Starting download...");
|
||||
Navigation.NavigateTo(url, true);
|
||||
//Navigation.NavigateTo(url, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -429,6 +429,6 @@
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
await SharedFileAccessService.Unregister(FileAccess);
|
||||
//await SharedFileAccessService.Unregister(FileAccess);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
@using Moonlight.Features.FileManager.Services
|
||||
@using MoonCore.Helpers
|
||||
|
||||
@inject EditorService EditorService
|
||||
|
||||
<div id="@Identifier" @onfocusout="FocusOut"></div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public string InitialContent { get; set; } = "";
|
||||
[Parameter] public string Theme { get; set; } = "one_dark";
|
||||
[Parameter] public string Mode { get; set; } = "text";
|
||||
[Parameter] public int Lines { get; set; } = 30;
|
||||
[Parameter] public int FontSize { get; set; } = 15;
|
||||
[Parameter] public bool EnableAutoInit { get; set; } = false;
|
||||
[Parameter] public Func<string, Task>? OnChanged { get; set; }
|
||||
|
||||
private string Identifier;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Identifier = "editor" + GetHashCode();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
if(EnableAutoInit)
|
||||
await Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
await EditorService.Create(
|
||||
Identifier,
|
||||
Theme,
|
||||
Mode,
|
||||
InitialContent,
|
||||
Lines,
|
||||
FontSize
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<string> GetContent() => await EditorService.GetValue();
|
||||
|
||||
public async Task SetContent(string content) => await EditorService.SetValue(content);
|
||||
|
||||
public async Task SetMode(string mode) => await EditorService.SetMode(mode);
|
||||
|
||||
private async Task FocusOut()
|
||||
{
|
||||
if (OnChanged != null)
|
||||
{
|
||||
var content = await GetContent();
|
||||
await OnChanged.Invoke(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
@using MoonCoreUI.Services
|
||||
@using Moonlight.Core.Services
|
||||
@using MoonCore.Helpers
|
||||
@using Moonlight.Core.Helpers
|
||||
@using Moonlight.Features.FileManager.Helpers
|
||||
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
|
||||
|
||||
@inject ToastService ToastService
|
||||
@inject HotKeyService HotKeyService
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<div class="card mb-2 border-0 rounded">
|
||||
<div class="card-body py-3 rounded" style="background-color: rgb(21, 21, 33)">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fw-bold fs-5 align-middle">@(File.Name) (@(Formatter.FormatSize(File.Size)))</span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<WButton OnClick="OnClose" CssClasses="btn btn-sm btn-primary">
|
||||
<i class="bx bx-sm bx-arrow-back"></i>Back
|
||||
</WButton>
|
||||
<WButton OnClick="OnSave" CssClasses="btn btn-sm btn-success">
|
||||
<i class="bx bx-sm bx-save"></i>Save
|
||||
</WButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Editor @ref="Editor" InitialContent="Loading file"/>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public FileEntry File { get; set; }
|
||||
|
||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||
|
||||
[Parameter] public bool CloseOnSave { get; set; } = false;
|
||||
|
||||
[Parameter] public Func<Task>? OnClosed { get; set; }
|
||||
|
||||
private Editor Editor;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// Initialize the editor
|
||||
await Editor.Initialize();
|
||||
|
||||
// Load file and check the file type
|
||||
var fileData = await FileAccess.ReadFile(File.Name);
|
||||
var mode = EditorModeDetector.GetModeFromFile(File.Name);
|
||||
|
||||
// Finalize editor
|
||||
await Editor.SetMode(mode);
|
||||
await Editor.SetContent(fileData);
|
||||
|
||||
HotKeyService.HotKeyPressed += OnHotKeyPressed;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnClose()
|
||||
{
|
||||
if (OnClosed != null)
|
||||
await OnClosed.Invoke();
|
||||
}
|
||||
|
||||
private async Task OnSave()
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = await Editor.GetContent();
|
||||
await FileAccess.WriteFile(File.Name, content);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn($"An unhandled error has occured while saving a file using access type {FileAccess.GetType().FullName}");
|
||||
Logger.Warn(e);
|
||||
|
||||
await ToastService.Danger("An unknown error has occured while saving the file. Please try again later");
|
||||
return;
|
||||
}
|
||||
|
||||
await ToastService.Success("Successfully saved file");
|
||||
|
||||
if (CloseOnSave)
|
||||
{
|
||||
if (OnClosed != null)
|
||||
await OnClosed.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnHotKeyPressed(string hotKey)
|
||||
{
|
||||
switch (hotKey)
|
||||
{
|
||||
case "save":
|
||||
await OnSave();
|
||||
break;
|
||||
case "close":
|
||||
await OnClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
HotKeyService.HotKeyPressed -= OnHotKeyPressed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
@using MoonCore.Helpers
|
||||
@using MoonCore.Services
|
||||
@using MoonCoreUI.Services
|
||||
@using Moonlight.Core.Configuration
|
||||
@using Moonlight.Core.Services
|
||||
@using Moonlight.Features.FileManager.Interfaces
|
||||
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
|
||||
@using Moonlight.Features.FileManager.Services
|
||||
|
||||
@inject AlertService AlertService
|
||||
@inject ToastService ToastService
|
||||
@inject FileManagerInteropService FileManagerInteropService
|
||||
@inject SharedFileAccessService FileAccessService
|
||||
@inject ConfigService<CoreConfiguration> ConfigService
|
||||
@inject NavigationManager Navigation
|
||||
@inject PluginService PluginService
|
||||
@inject IServiceProvider ServiceProvider
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<div class="card card-body px-5">
|
||||
<div class="d-flex justify-content-center justify-content-md-between">
|
||||
<div class="d-none d-md-flex justify-content-start align-items-center">
|
||||
<div class="badge badge-primary badge-lg fs-5 py-2 text-center">
|
||||
@{
|
||||
var parts = Path
|
||||
.Split("/")
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.ToArray();
|
||||
|
||||
var i = 1;
|
||||
}
|
||||
|
||||
<a href="#" class="text-white mx-1" @onclick:preventDefault @onclick="() => NavigateBackToLevel(0)">/</a>
|
||||
|
||||
@foreach (var part in parts)
|
||||
{
|
||||
var x = i + 0;
|
||||
|
||||
<a href="#" class="text-white mx-1" @onclick:preventDefault @onclick="() => NavigateBackToLevel(x)">@(part)</a>
|
||||
<div class="mx-2 text-white">/</div>
|
||||
|
||||
i++;
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center justify-content-md-end align-items-center">
|
||||
<WButton OnClick="ManualRefresh" CssClasses="btn btn-icon btn-light-info">
|
||||
<i class="bx bx-sm bx-refresh"></i>
|
||||
</WButton>
|
||||
<label for="fileManagerSelect" class="btn btn-light-primary mx-2">Upload</label>
|
||||
<input id="fileManagerSelect" type="file" hidden="hidden" multiple/>
|
||||
<div class="dropdown">
|
||||
<a class="btn btn-primary dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
New
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuLink" style="">
|
||||
<li>
|
||||
<a href="#" class="dropdown-item" @onclick:preventDefault @onclick="CreateFile">
|
||||
<i class="bx bx-sm bx-file text-primary me-2 align-middle"></i>
|
||||
<span class="align-middle fs-6">File</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="dropdown-item" @onclick:preventDefault @onclick="CreateDirectory">
|
||||
<i class="bx bx-sm bx-folder text-primary me-2 align-middle"></i>
|
||||
<span class="align-middle fs-6">Folder</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (ShowEditor)
|
||||
{
|
||||
<div class="card card-body px-2 py-2 mt-5">
|
||||
<FileEditor @ref="Editor" FileAccess="FileAccess" File="FileToEdit" OnClosed="CloseEditor"/>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="fileManagerUpload" class="card card-body px-5 py-3 mt-5">
|
||||
<FileView @ref="View"
|
||||
FileAccess="FileAccess"
|
||||
OnEntryClicked="OnEntryClicked"
|
||||
OnNavigateUpClicked="OnNavigateUpClicked"
|
||||
EnableContextMenu="true">
|
||||
<ContextMenuTemplate>
|
||||
@foreach (var action in Actions)
|
||||
{
|
||||
if(!action.Filter.Invoke(context))
|
||||
continue;
|
||||
|
||||
<a class="dropdown-item" href="#" @onclick:preventDefault @onclick="() => InvokeContextAction(action, context)">
|
||||
<i class="bx bx-sm @action.Icon text-@action.Color align-middle"></i>
|
||||
<span class="align-middle ms-3">@action.Name</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
<a class="dropdown-item" href="#" @onclick:preventDefault @onclick="() => Move(context)">
|
||||
<i class="bx bx-sm bx-move text-info align-middle"></i>
|
||||
<span class="align-middle ms-3">Move</span>
|
||||
</a>
|
||||
</ContextMenuTemplate>
|
||||
</FileView>
|
||||
</div>
|
||||
|
||||
<SmartModal @ref="MoveModal" CssClasses="modal-lg modal-dialog-centered">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Select a new location</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="HideMove"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<FileView @ref="MoveView"
|
||||
FileAccess="MoveAccess"
|
||||
Filter="FolderOnlyFilter"
|
||||
ShowDate="false"
|
||||
ShowSelect="false"
|
||||
ShowSize="false"
|
||||
OnEntryClicked="OnFolderClicked"
|
||||
OnNavigateUpClicked="OnMoveUpClicked"
|
||||
EnableContextMenu="false"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="HideMove">Close</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="FinishMove">Save changes</button>
|
||||
</div>
|
||||
</SmartModal>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||
|
||||
private FileView View;
|
||||
private string Path = "/";
|
||||
|
||||
private IFileManagerAction[] Actions;
|
||||
|
||||
// Editor
|
||||
private FileEditor Editor;
|
||||
private FileEntry FileToEdit;
|
||||
private bool ShowEditor = false;
|
||||
|
||||
// Move
|
||||
private SmartModal MoveModal;
|
||||
private BaseFileAccess MoveAccess;
|
||||
private FileView MoveView;
|
||||
private bool InMoveState = false;
|
||||
private Func<FileEntry, bool> FolderOnlyFilter = entry => entry.IsDirectory;
|
||||
private Func<FileEntry, Task> OnFolderClicked;
|
||||
private Func<Task> OnMoveUpClicked;
|
||||
private List<FileEntry> FilesToMove = new();
|
||||
|
||||
private Timer? UploadTokenTimer;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
OnFolderClicked = async entry =>
|
||||
{
|
||||
await MoveAccess.ChangeDirectory(entry.Name);
|
||||
await MoveView.Refresh();
|
||||
};
|
||||
OnMoveUpClicked = async () =>
|
||||
{
|
||||
await MoveAccess.ChangeDirectory("..");
|
||||
await MoveView.Refresh();
|
||||
};
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender)
|
||||
return;
|
||||
|
||||
// Load plugin ui and options
|
||||
Actions = await PluginService.GetImplementations<IFileManagerAction>();
|
||||
|
||||
|
||||
// Setup upload url update timer
|
||||
UploadTokenTimer = new(async _ =>
|
||||
{
|
||||
await FileAccessService.Register(FileAccess);
|
||||
var token = await FileAccessService.GenerateToken(FileAccess);
|
||||
var url = $"/api/upload?token={token}";
|
||||
|
||||
await FileManagerInteropService.UpdateUrl("fileManager", url);
|
||||
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
||||
|
||||
// Create initial url
|
||||
await FileAccessService.Register(FileAccess);
|
||||
var token = await FileAccessService.GenerateToken(FileAccess);
|
||||
var url = $"/api/upload?token={token}";
|
||||
|
||||
// Refresh the file view when a upload is completed
|
||||
FileManagerInteropService.OnUploadStateChanged += async () => { await View.Refresh(); };
|
||||
|
||||
// Initialize drop area & file select
|
||||
await FileManagerInteropService.UpdateUrl("fileManager", url);
|
||||
await FileManagerInteropService.InitDropzone("fileManagerUpload", "fileManager");
|
||||
await FileManagerInteropService.InitFileSelect("fileManagerSelect", "fileManager");
|
||||
}
|
||||
|
||||
private async Task OnEntryClicked(FileEntry entry)
|
||||
{
|
||||
if (entry.IsFile)
|
||||
{
|
||||
var fileSizeInKilobytes = ByteSizeValue.FromBytes(entry.Size).KiloBytes;
|
||||
|
||||
if (fileSizeInKilobytes > ConfigService.Get().Customisation.FileManager.MaxFileOpenSize)
|
||||
{
|
||||
await ToastService.Danger("Unable to open file as it exceeds the max file size limit");
|
||||
return;
|
||||
}
|
||||
|
||||
await OpenEditor(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
await FileAccess.ChangeDirectory(entry.Name);
|
||||
await View.Refresh();
|
||||
|
||||
await Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InvokeContextAction(IFileManagerAction action, FileEntry entry)
|
||||
{
|
||||
await View.HideContextMenu();
|
||||
|
||||
await action.Execute(FileAccess, View, entry, ServiceProvider);
|
||||
}
|
||||
|
||||
#region Navigation & Refreshing
|
||||
|
||||
private async Task OnNavigateUpClicked()
|
||||
{
|
||||
await FileAccess.ChangeDirectory("..");
|
||||
await View.Refresh();
|
||||
|
||||
await Refresh();
|
||||
}
|
||||
|
||||
private async Task NavigateBackToLevel(int level)
|
||||
{
|
||||
if (ShowEditor) // Ignore navigation events while the editor is open
|
||||
return;
|
||||
|
||||
var path = await FileAccess.GetCurrentDirectory();
|
||||
|
||||
var parts = path.Split("/");
|
||||
var pathToNavigate = string.Join("/", parts.Take(level + 1)) + "/";
|
||||
|
||||
await FileAccess.SetDirectory(pathToNavigate);
|
||||
await View.Refresh();
|
||||
await Refresh();
|
||||
}
|
||||
|
||||
private async Task ManualRefresh()
|
||||
{
|
||||
if (ShowEditor) // Ignore refresh while editor is open
|
||||
return;
|
||||
|
||||
await View.Refresh();
|
||||
await Refresh();
|
||||
|
||||
await ToastService.Info("Refreshed");
|
||||
}
|
||||
|
||||
private async Task Refresh()
|
||||
{
|
||||
Path = await FileAccess.GetCurrentDirectory();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Actions
|
||||
|
||||
private async Task DeleteSelection()
|
||||
{
|
||||
var itemsToDelete = View.Selection;
|
||||
|
||||
await ToastService.CreateProgress("fileManagerDeleteFile", "Deleting items");
|
||||
|
||||
var i = 1;
|
||||
foreach (var entry in itemsToDelete)
|
||||
{
|
||||
await ToastService.ModifyProgress("fileManagerDeleteFile", $"[{i}/{FilesToMove.Count}] Deleting items");
|
||||
await FileAccess.Delete(entry);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
await ToastService.RemoveProgress("fileManagerDeleteFile");
|
||||
|
||||
await ToastService.Success($"Successfully deleted {FilesToMove.Count} items");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Create Dir / File
|
||||
|
||||
private async Task CreateDirectory()
|
||||
{
|
||||
var name = await AlertService.Text("Enter a name for the new directory");
|
||||
|
||||
if (string.IsNullOrEmpty(name) || name.Contains(".."))
|
||||
return;
|
||||
|
||||
await FileAccess.CreateDirectory(name);
|
||||
|
||||
await ToastService.Success("Successfully created directory");
|
||||
await View.Refresh();
|
||||
}
|
||||
|
||||
private async Task CreateFile()
|
||||
{
|
||||
var name = await AlertService.Text("Enter a name for the new file");
|
||||
|
||||
if (string.IsNullOrEmpty(name) || name.Contains(".."))
|
||||
return;
|
||||
|
||||
await FileAccess.CreateFile(name);
|
||||
|
||||
// We build a virtual entry here so we dont need to fetch one
|
||||
await OpenEditor(new()
|
||||
{
|
||||
Name = name,
|
||||
Size = 0,
|
||||
IsFile = true,
|
||||
IsDirectory = false,
|
||||
LastModifiedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File Editor
|
||||
|
||||
private async Task OpenEditor(FileEntry entry)
|
||||
{
|
||||
FileToEdit = entry;
|
||||
ShowEditor = true;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task CloseEditor()
|
||||
{
|
||||
ShowEditor = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Move
|
||||
|
||||
private async Task Move(FileEntry entry)
|
||||
{
|
||||
await View.HideContextMenu();
|
||||
|
||||
FilesToMove.Clear();
|
||||
|
||||
FilesToMove.Add(entry);
|
||||
|
||||
await StartMove();
|
||||
}
|
||||
|
||||
private async Task MoveSelection()
|
||||
{
|
||||
FilesToMove.Clear();
|
||||
|
||||
FilesToMove.AddRange(View.Selection);
|
||||
|
||||
await StartMove();
|
||||
}
|
||||
|
||||
private async Task StartMove()
|
||||
{
|
||||
// Cleanup if modal was removed in any other way
|
||||
if (InMoveState)
|
||||
await HideMove();
|
||||
|
||||
// Prepare file access and show modal
|
||||
InMoveState = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
MoveAccess = FileAccess.Clone();
|
||||
await MoveAccess.SetDirectory("/");
|
||||
|
||||
await MoveModal.Show();
|
||||
}
|
||||
|
||||
private async Task HideMove()
|
||||
{
|
||||
await MoveModal.Hide();
|
||||
MoveAccess.Dispose();
|
||||
|
||||
InMoveState = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task FinishMove()
|
||||
{
|
||||
var target = await MoveAccess.GetCurrentDirectory();
|
||||
|
||||
await HideMove();
|
||||
|
||||
await ToastService.CreateProgress("fileManagerMoveFile", "Moving items");
|
||||
|
||||
var i = 1;
|
||||
foreach (var entry in FilesToMove)
|
||||
{
|
||||
await ToastService.ModifyProgress("fileManagerMoveFile", $"[{i}/{FilesToMove.Count}] Moving items");
|
||||
await FileAccess.Move(entry, target + entry.Name);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
await ToastService.RemoveProgress("fileManagerMoveFile");
|
||||
|
||||
await ToastService.Success($"Successfully moved {FilesToMove.Count} items");
|
||||
|
||||
await View.Refresh();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
if (UploadTokenTimer != null)
|
||||
await UploadTokenTimer.DisposeAsync();
|
||||
|
||||
await FileAccessService.Unregister(FileAccess);
|
||||
}
|
||||
}
|
||||
387
Moonlight/Features/FileManager/UI/NewFileManager/FileView.razor
Normal file
387
Moonlight/Features/FileManager/UI/NewFileManager/FileView.razor
Normal file
@@ -0,0 +1,387 @@
|
||||
@using MoonCore.Helpers
|
||||
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
|
||||
@using BlazorContextMenu
|
||||
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<div class="@(IsLoading ? "table-loading" : "")">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="table-loading-message table-loading-message fs-3 fw-bold text-white">
|
||||
Loading...
|
||||
</div>
|
||||
}
|
||||
<table class="w-100 table table-row-bordered @(IsLoading ? "blur" : "table-hover") fs-6">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
@if (ShowSelect)
|
||||
{
|
||||
<td class="w-10px align-middle">
|
||||
<div class="form-check">
|
||||
@if (IsAllSelected)
|
||||
{
|
||||
<input class="form-check-input" type="checkbox" value="1" checked="checked" @oninput="() => ChangeAllSelection(false)">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input class="form-check-input" type="checkbox" value="0" @oninput="() => ChangeAllSelection(true)">
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
<td class="w-10px"></td>
|
||||
<td>Name</td>
|
||||
@if (ShowSize)
|
||||
{
|
||||
<td class="d-none d-md-table-cell">Size</td>
|
||||
}
|
||||
@if (ShowDate)
|
||||
{
|
||||
<td class="d-none d-md-table-cell">Last modified</td>
|
||||
}
|
||||
@if (EnableContextMenu)
|
||||
{
|
||||
<td></td>
|
||||
}
|
||||
@if (AdditionTemplate != null)
|
||||
{
|
||||
<td></td>
|
||||
}
|
||||
</tr>
|
||||
|
||||
@if (Path != "/" && ShowNavigateUp)
|
||||
{
|
||||
<tr class="fw-semibold">
|
||||
@if (ShowSelect)
|
||||
{
|
||||
<td class="align-middle w-10px"></td>
|
||||
}
|
||||
<td class="w-10px">
|
||||
<i class="bx bx-sm bx-chevrons-left"></i>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" @onclick:preventDefault @onclick="NavigateUp">
|
||||
Back to parent folder
|
||||
</a>
|
||||
</td>
|
||||
@if (ShowSize)
|
||||
{
|
||||
<td></td>
|
||||
}
|
||||
@if (ShowDate)
|
||||
{
|
||||
<td></td>
|
||||
}
|
||||
@if (EnableContextMenu)
|
||||
{
|
||||
<td></td>
|
||||
}
|
||||
@if (AdditionTemplate != null)
|
||||
{
|
||||
<td></td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
|
||||
@foreach (var entry in Entries)
|
||||
{
|
||||
if (EnableContextMenu)
|
||||
{
|
||||
<ContextMenuTrigger MenuId="@ContextMenuId" WrapperTag="tr" Data="entry">
|
||||
@if (ShowSelect)
|
||||
{
|
||||
<td class="w-10px align-middle">
|
||||
<div class="form-check">
|
||||
@if (SelectionCache.ContainsKey(entry) && SelectionCache[entry])
|
||||
{
|
||||
<input class="form-check-input" type="checkbox" value="1" checked="checked" @oninput="() => ChangeSelection(entry, false)">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input class="form-check-input" type="checkbox" value="0" @oninput="() => ChangeSelection(entry, true)">
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
<td class="align-middle w-10px">
|
||||
@if (entry.IsFile)
|
||||
{
|
||||
<i class="bx bx-md bxs-file-blank text-white"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bx bx-md bxs-folder text-primary"></i>
|
||||
}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<a href="#" @onclick:preventDefault @onclick="() => HandleEntryClick(entry)">
|
||||
@entry.Name
|
||||
</a>
|
||||
</td>
|
||||
@if (ShowSize)
|
||||
{
|
||||
<td class="align-middle d-none d-md-table-cell">
|
||||
@if (entry.IsFile)
|
||||
{
|
||||
@Formatter.FormatSize(entry.Size)
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (ShowDate)
|
||||
{
|
||||
<td class="align-middle d-none d-md-table-cell">
|
||||
@Formatter.FormatDate(entry.LastModifiedAt)
|
||||
</td>
|
||||
}
|
||||
<td class="d-table-cell d-md-none">
|
||||
<div class="dropstart">
|
||||
<button class="btn btn-icon btn-secondary" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bx bx-sm bx-dots-horizontal"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu fs-6">
|
||||
@if (ContextMenuTemplate != null)
|
||||
{
|
||||
@ContextMenuTemplate.Invoke(entry)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@if (AdditionTemplate != null)
|
||||
{
|
||||
@AdditionTemplate.Invoke(entry)
|
||||
}
|
||||
</ContextMenuTrigger>
|
||||
}
|
||||
else
|
||||
{
|
||||
<tr>
|
||||
@if (ShowSelect)
|
||||
{
|
||||
<td class="w-10px align-middle">
|
||||
<div class="form-check">
|
||||
@if (SelectionCache.ContainsKey(entry) && SelectionCache[entry])
|
||||
{
|
||||
<input class="form-check-input" type="checkbox" value="1" checked="checked" @oninput="() => ChangeSelection(entry, false)">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input class="form-check-input" type="checkbox" value="0" @oninput="() => ChangeSelection(entry, true)">
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
<td class="align-middle w-10px">
|
||||
@if (entry.IsFile)
|
||||
{
|
||||
<i class="bx bx-md bxs-file-blank text-white"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bx bx-md bxs-folder text-primary"></i>
|
||||
}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<a href="#" @onclick:preventDefault @onclick="() => HandleEntryClick(entry)">
|
||||
@entry.Name
|
||||
</a>
|
||||
</td>
|
||||
@if (ShowSize)
|
||||
{
|
||||
<td class="align-middle d-none d-md-table-cell">
|
||||
@if (entry.IsFile)
|
||||
{
|
||||
@Formatter.FormatSize(entry.Size)
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (ShowDate)
|
||||
{
|
||||
<td class="align-middle d-none d-md-table-cell">
|
||||
@Formatter.FormatDate(entry.LastModifiedAt)
|
||||
</td>
|
||||
}
|
||||
@if (AdditionTemplate != null)
|
||||
{
|
||||
@AdditionTemplate.Invoke(entry)
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (EnableContextMenu && ContextMenuTemplate != null)
|
||||
{
|
||||
<ContextMenu @ref="CurrentContextMenu" Id="@ContextMenuId" OnAppearing="OnContextMenuAppear" OnHiding="OnContextMenuHide">
|
||||
@if (ShowContextMenu)
|
||||
{
|
||||
<div class="dropdown-menu show fs-6">
|
||||
@ContextMenuTemplate.Invoke(ContextMenuItem)
|
||||
</div>
|
||||
}
|
||||
</ContextMenu>
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public RenderFragment<FileEntry>? AdditionTemplate { get; set; }
|
||||
|
||||
[Parameter] public bool ShowSize { get; set; } = true;
|
||||
[Parameter] public bool ShowDate { get; set; } = true;
|
||||
[Parameter] public bool ShowSelect { get; set; } = true;
|
||||
[Parameter] public bool ShowNavigateUp { get; set; } = true;
|
||||
|
||||
[Parameter] public RenderFragment<FileEntry>? ContextMenuTemplate { get; set; }
|
||||
[Parameter] public bool EnableContextMenu { get; set; } = false;
|
||||
private bool ShowContextMenu = false;
|
||||
private FileEntry ContextMenuItem;
|
||||
private string ContextMenuId = "fileManagerContextMenu";
|
||||
private ContextMenu? CurrentContextMenu;
|
||||
|
||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||
[Parameter] public Func<FileEntry, bool>? Filter { get; set; }
|
||||
|
||||
[Parameter] public Func<FileEntry, Task>? OnEntryClicked { get; set; }
|
||||
[Parameter] public Func<FileEntry[], Task>? OnSelectionChanged { get; set; }
|
||||
|
||||
[Parameter] public Func<Task>? OnNavigateUpClicked { get; set; }
|
||||
|
||||
private bool IsLoading = false;
|
||||
private string LoadingText = "";
|
||||
|
||||
private FileEntry[] Entries = Array.Empty<FileEntry>();
|
||||
private string Path = "/";
|
||||
|
||||
private Dictionary<FileEntry, bool> SelectionCache = new();
|
||||
public FileEntry[] Selection => SelectionCache.Where(x => x.Value).Select(x => x.Key).ToArray();
|
||||
private bool IsAllSelected => Entries.Length != 0 && SelectionCache.Count(x => x.Value) == Entries.Length;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
await Refresh();
|
||||
}
|
||||
|
||||
public async Task Refresh()
|
||||
{
|
||||
IsLoading = true;
|
||||
LoadingText = "Loading";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// Load current directory
|
||||
Path = await FileAccess.GetCurrentDirectory();
|
||||
|
||||
// Load entries
|
||||
LoadingText = "Loading files and folders";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
Entries = await FileAccess.List();
|
||||
|
||||
// Sort entries
|
||||
LoadingText = "Sorting files and folders";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (Filter != null)
|
||||
{
|
||||
Entries = Entries
|
||||
.Where(x => Filter.Invoke(x))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
Entries = Entries
|
||||
.GroupBy(x => x.IsFile)
|
||||
.OrderBy(x => x.Key)
|
||||
.SelectMany(x => x.OrderBy(y => y.Name))
|
||||
.ToArray();
|
||||
|
||||
// Build selection cache
|
||||
SelectionCache.Clear();
|
||||
|
||||
foreach (var entry in Entries)
|
||||
SelectionCache.Add(entry, false);
|
||||
|
||||
if (OnSelectionChanged != null)
|
||||
await OnSelectionChanged.Invoke(Array.Empty<FileEntry>());
|
||||
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task HandleEntryClick(FileEntry entry)
|
||||
{
|
||||
if (OnEntryClicked == null)
|
||||
return;
|
||||
|
||||
await OnEntryClicked.Invoke(entry);
|
||||
}
|
||||
|
||||
private async Task NavigateUp()
|
||||
{
|
||||
if (OnNavigateUpClicked == null)
|
||||
return;
|
||||
|
||||
await OnNavigateUpClicked.Invoke();
|
||||
}
|
||||
|
||||
#region Selection
|
||||
|
||||
private async Task ChangeSelection(FileEntry entry, bool selectionState)
|
||||
{
|
||||
SelectionCache[entry] = selectionState;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (OnSelectionChanged != null)
|
||||
{
|
||||
await OnSelectionChanged.Invoke(SelectionCache
|
||||
.Where(x => x.Value)
|
||||
.Select(x => x.Key)
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ChangeAllSelection(bool toggle)
|
||||
{
|
||||
foreach (var key in SelectionCache.Keys)
|
||||
SelectionCache[key] = toggle;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (OnSelectionChanged != null)
|
||||
{
|
||||
await OnSelectionChanged.Invoke(SelectionCache
|
||||
.Where(x => x.Value)
|
||||
.Select(x => x.Key)
|
||||
.ToArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context Menu
|
||||
|
||||
private async Task OnContextMenuAppear(MenuAppearingEventArgs data)
|
||||
{
|
||||
ContextMenuItem = (data.Data as FileEntry)!;
|
||||
|
||||
ShowContextMenu = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnContextMenuHide()
|
||||
{
|
||||
ShowContextMenu = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async Task HideContextMenu()
|
||||
{
|
||||
ShowContextMenu = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user