Files
Moonlight/Moonlight/Features/FileManager/UI/Components/FileManager.razor
2024-05-13 16:20:38 +02:00

364 lines
12 KiB
Plaintext

@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 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">
@if (View != null && View.Selection.Any())
{
foreach (var action in SelectionActions)
{
var cssClass = $"btn btn-{action.Color} mx-2";
<WButton Text="@action.Name" CssClasses="@cssClass" OnClick="() => InvokeSelectionAction(action)"/>
}
}
else
{
<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">
@foreach (var action in CreateActions)
{
<li>
<a href="#" class="dropdown-item" @onclick:preventDefault @onclick="() => InvokeCreateAction(action)">
<i class="bx bx-sm @action.Icon text-@action.Color me-2 align-middle"></i>
<span class="align-middle fs-6">@action.Name</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"
OnSelectionChanged="OnSelectionChanged"
EnableContextMenu="true"
ShowUploadPrompt="true">
<ContextMenuTemplate>
@foreach (var action in ContextActions)
{
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>
}
</ContextMenuTemplate>
</FileView>
</div>
<SmartModal @ref="FolderSelectModal" CssClasses="modal-lg modal-dialog-centered">
<div class="modal-header">
<h5 class="modal-title">@FolderSelectTitle</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="HideFolderSelect"></button>
</div>
<div class="modal-body">
<FileView @ref="FolderSelectView"
FileAccess="FolderSelectFileAccess"
Filter="FolderSelectFilter"
ShowDate="false"
ShowSelect="false"
ShowSize="false"
OnEntryClicked="EntryClickFolderSelect"
OnNavigateUpClicked="NavigateUpFolderSelect"
EnableContextMenu="false"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="HideFolderSelect">Cancel</button>
<button type="button" class="btn btn-primary" @onclick="SubmitFolderSelect">Submit</button>
</div>
</SmartModal>
}
@code
{
[Parameter] public BaseFileAccess FileAccess { get; set; }
public FileView View { get; private set; }
private string Path = "/";
private IFileManagerContextAction[] ContextActions;
private IFileManagerSelectionAction[] SelectionActions;
private IFileManagerCreateAction[] CreateActions;
// Editor
private FileEditor Editor;
private FileEntry FileToEdit;
private bool ShowEditor = false;
// Folder select dialog
private bool FolderSelectIsOpen = false;
private SmartModal FolderSelectModal;
private BaseFileAccess FolderSelectFileAccess;
private string FolderSelectTitle;
private Func<string, Task> FolderSelectResult;
private FileView FolderSelectView;
private Func<FileEntry, bool> FolderSelectFilter => entry => entry.IsDirectory;
private Timer? UploadTokenTimer;
protected override async Task OnInitializedAsync()
{
// Load plugin ui and options
ContextActions = await PluginService.GetImplementations<IFileManagerContextAction>();
SelectionActions = await PluginService.GetImplementations<IFileManagerSelectionAction>();
CreateActions = await PluginService.GetImplementations<IFileManagerCreateAction>();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
// 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(IFileManagerContextAction contextAction, FileEntry entry)
{
await View.HideContextMenu();
await contextAction.Execute(FileAccess, this, entry, ServiceProvider);
}
private async Task InvokeSelectionAction(IFileManagerSelectionAction action)
{
await action.Execute(FileAccess, this, View.Selection, ServiceProvider);
// Refresh resets the selection
await View.Refresh();
}
private async Task InvokeCreateAction(IFileManagerCreateAction action)
{
await action.Execute(FileAccess, this, ServiceProvider);
}
private async Task OnSelectionChanged(FileEntry[] _) => await InvokeAsync(StateHasChanged);
#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 File Editor
public async Task OpenEditor(FileEntry entry)
{
FileToEdit = entry;
ShowEditor = true;
await InvokeAsync(StateHasChanged);
}
public async Task CloseEditor()
{
ShowEditor = false;
await InvokeAsync(StateHasChanged);
}
#endregion
#region Selects
public async Task OpenFolderSelect(string title, Func<string, Task> onResult)
{
if (FolderSelectIsOpen)
await HideFolderSelect();
FolderSelectResult = onResult;
FolderSelectTitle = title;
FolderSelectFileAccess = FileAccess.Clone();
await FolderSelectFileAccess.SetDirectory("/");
await FolderSelectModal.Show();
}
public async Task HideFolderSelect()
{
await FolderSelectModal.Hide();
FolderSelectIsOpen = false;
FolderSelectFileAccess.Dispose();
}
private async Task SubmitFolderSelect()
{
var path = await FolderSelectFileAccess.GetCurrentDirectory();
await HideFolderSelect();
await FolderSelectResult.Invoke(path);
}
private async Task NavigateUpFolderSelect()
{
await FolderSelectFileAccess.ChangeDirectory("..");
await FolderSelectView.Refresh();
}
private async Task EntryClickFolderSelect(FileEntry entry)
{
await FolderSelectFileAccess.ChangeDirectory(entry.Name);
await FolderSelectView.Refresh();
}
#endregion
public async void Dispose()
{
if (UploadTokenTimer != null)
await UploadTokenTimer.DisposeAsync();
await FileAccessService.Unregister(FileAccess);
}
}