Improved file manager, switched to faster node/daemon file communication. Removed old components
This commit is contained in:
parent
9abf32b288
commit
20fcd5015e
26 changed files with 680 additions and 1841 deletions
|
@ -40,18 +40,26 @@ public class HostFileActions : IFileActions
|
||||||
return Task.FromResult(entries.ToArray());
|
return Task.FromResult(entries.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Delete(string path)
|
public Task DeleteFile(string path)
|
||||||
{
|
{
|
||||||
var fullPath = GetFullPath(path);
|
var fullPath = GetFullPath(path);
|
||||||
|
|
||||||
if (File.Exists(fullPath))
|
if (File.Exists(fullPath))
|
||||||
File.Delete(fullPath);
|
File.Delete(fullPath);
|
||||||
else if (Directory.Exists(fullPath))
|
|
||||||
Directory.Delete(fullPath, true);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task DeleteDirectory(string path)
|
||||||
|
{
|
||||||
|
var fullPath = GetFullPath(path);
|
||||||
|
|
||||||
|
if (Directory.Exists(fullPath))
|
||||||
|
Directory.Delete(fullPath, true);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public Task Move(string from, string to)
|
public Task Move(string from, string to)
|
||||||
{
|
{
|
||||||
var source = GetFullPath(from);
|
var source = GetFullPath(from);
|
||||||
|
@ -65,29 +73,29 @@ public class HostFileActions : IFileActions
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CreateDirectory(string name)
|
public Task CreateDirectory(string path)
|
||||||
{
|
{
|
||||||
var fullPath = GetFullPath(name);
|
var fullPath = GetFullPath(path);
|
||||||
Directory.CreateDirectory(fullPath);
|
Directory.CreateDirectory(fullPath);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CreateFile(string name)
|
public Task CreateFile(string path)
|
||||||
{
|
{
|
||||||
var fullPath = GetFullPath(name);
|
var fullPath = GetFullPath(path);
|
||||||
File.Create(fullPath).Close();
|
File.Create(fullPath).Close();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> ReadFile(string name)
|
public Task<string> ReadFile(string path)
|
||||||
{
|
{
|
||||||
var fullPath = GetFullPath(name);
|
var fullPath = GetFullPath(path);
|
||||||
return File.ReadAllTextAsync(fullPath);
|
return File.ReadAllTextAsync(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteFile(string name, string content)
|
public Task WriteFile(string path, string content)
|
||||||
{
|
{
|
||||||
var fullPath = GetFullPath(name);
|
var fullPath = GetFullPath(path);
|
||||||
|
|
||||||
EnsureDir(fullPath);
|
EnsureDir(fullPath);
|
||||||
|
|
||||||
|
@ -95,15 +103,15 @@ public class HostFileActions : IFileActions
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Stream> ReadFileStream(string name)
|
public Task<Stream> ReadFileStream(string path)
|
||||||
{
|
{
|
||||||
var fullPath = GetFullPath(name);
|
var fullPath = GetFullPath(path);
|
||||||
return Task.FromResult<Stream>(File.OpenRead(fullPath));
|
return Task.FromResult<Stream>(File.OpenRead(fullPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteFileStream(string name, Stream dataStream)
|
public Task WriteFileStream(string path, Stream dataStream)
|
||||||
{
|
{
|
||||||
var fullPath = GetFullPath(name);
|
var fullPath = GetFullPath(path);
|
||||||
|
|
||||||
EnsureDir(fullPath);
|
EnsureDir(fullPath);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using MoonCoreUI.Services;
|
using MoonCoreUI.Services;
|
||||||
using Moonlight.Features.FileManager.Interfaces;
|
using Moonlight.Features.FileManager.Interfaces;
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Implementations;
|
namespace Moonlight.Features.FileManager.Implementations;
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@ public class CreateFileAction : IFileManagerCreateAction
|
||||||
public string Icon => "bx-file";
|
public string Icon => "bx-file";
|
||||||
public string Color => "primary";
|
public string Color => "primary";
|
||||||
|
|
||||||
public async Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, IServiceProvider provider)
|
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, IServiceProvider provider)
|
||||||
{
|
{
|
||||||
var alertService = provider.GetRequiredService<AlertService>();
|
var alertService = provider.GetRequiredService<AlertService>();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using MoonCoreUI.Services;
|
using MoonCoreUI.Services;
|
||||||
using Moonlight.Features.FileManager.Interfaces;
|
using Moonlight.Features.FileManager.Interfaces;
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
using Moonlight.Features.FileManager.UI.Components;
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Implementations;
|
namespace Moonlight.Features.FileManager.Implementations;
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ public class CreateFolderAction : IFileManagerCreateAction
|
||||||
public string Icon => "bx-folder";
|
public string Icon => "bx-folder";
|
||||||
public string Color => "primary";
|
public string Color => "primary";
|
||||||
|
|
||||||
public async Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, IServiceProvider provider)
|
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, IServiceProvider provider)
|
||||||
{
|
{
|
||||||
var alertService = provider.GetRequiredService<AlertService>();
|
var alertService = provider.GetRequiredService<AlertService>();
|
||||||
var toastService = provider.GetRequiredService<ToastService>();
|
var toastService = provider.GetRequiredService<ToastService>();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using MoonCoreUI.Services;
|
using MoonCoreUI.Services;
|
||||||
using Moonlight.Features.FileManager.Interfaces;
|
using Moonlight.Features.FileManager.Interfaces;
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
using Moonlight.Features.FileManager.UI.Components;
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Implementations;
|
namespace Moonlight.Features.FileManager.Implementations;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ public class DeleteContextAction : IFileManagerContextAction
|
||||||
public string Color => "danger";
|
public string Color => "danger";
|
||||||
public Func<FileEntry, bool> Filter => _ => true;
|
public Func<FileEntry, bool> Filter => _ => true;
|
||||||
|
|
||||||
public async Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, FileEntry entry, IServiceProvider serviceProvider)
|
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
await access.Delete(entry);
|
await access.Delete(entry);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using MoonCoreUI.Services;
|
using MoonCoreUI.Services;
|
||||||
using Moonlight.Features.FileManager.Interfaces;
|
using Moonlight.Features.FileManager.Interfaces;
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
using Moonlight.Features.FileManager.UI.Components;
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Implementations;
|
namespace Moonlight.Features.FileManager.Implementations;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ public class DeleteSelectionAction : IFileManagerSelectionAction
|
||||||
public string Name => "Delete";
|
public string Name => "Delete";
|
||||||
public string Color => "danger";
|
public string Color => "danger";
|
||||||
|
|
||||||
public async Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, FileEntry[] entries, IServiceProvider provider)
|
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry[] entries, IServiceProvider provider)
|
||||||
{
|
{
|
||||||
var alertService = provider.GetRequiredService<AlertService>();
|
var alertService = provider.GetRequiredService<AlertService>();
|
||||||
var toastService = provider.GetRequiredService<ToastService>();
|
var toastService = provider.GetRequiredService<ToastService>();
|
||||||
|
|
|
@ -4,7 +4,7 @@ using MoonCoreUI.Services;
|
||||||
using Moonlight.Features.FileManager.Interfaces;
|
using Moonlight.Features.FileManager.Interfaces;
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.Services;
|
using Moonlight.Features.FileManager.Services;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
using Moonlight.Features.FileManager.UI.Components;
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Implementations;
|
namespace Moonlight.Features.FileManager.Implementations;
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ public class DownloadContextAction : IFileManagerContextAction
|
||||||
public string Color => "primary";
|
public string Color => "primary";
|
||||||
public Func<FileEntry, bool> Filter => entry => entry.IsFile;
|
public Func<FileEntry, bool> Filter => entry => entry.IsFile;
|
||||||
|
|
||||||
public async Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, FileEntry entry, IServiceProvider serviceProvider)
|
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
var fileAccessService = serviceProvider.GetRequiredService<SharedFileAccessService>();
|
var fileAccessService = serviceProvider.GetRequiredService<SharedFileAccessService>();
|
||||||
var navigation = serviceProvider.GetRequiredService<NavigationManager>();
|
var navigation = serviceProvider.GetRequiredService<NavigationManager>();
|
||||||
|
|
|
@ -11,7 +11,7 @@ public class MoveContextAction : IFileManagerContextAction
|
||||||
public string Color => "info";
|
public string Color => "info";
|
||||||
public Func<FileEntry, bool> Filter => _ => true;
|
public Func<FileEntry, bool> Filter => _ => true;
|
||||||
|
|
||||||
public async Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, FileEntry entry, IServiceProvider provider)
|
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider provider)
|
||||||
{
|
{
|
||||||
await fileManager.OpenFolderSelect("Select the location to move the item to", async path =>
|
await fileManager.OpenFolderSelect("Select the location to move the item to", async path =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,7 +9,7 @@ public class MoveSelectionAction : IFileManagerSelectionAction
|
||||||
public string Name => "Move";
|
public string Name => "Move";
|
||||||
public string Color => "primary";
|
public string Color => "primary";
|
||||||
|
|
||||||
public async Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, FileEntry[] entries, IServiceProvider provider)
|
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry[] entries, IServiceProvider provider)
|
||||||
{
|
{
|
||||||
await fileManager.OpenFolderSelect("Select the location to move the items to", async path =>
|
await fileManager.OpenFolderSelect("Select the location to move the items to", async path =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using MoonCoreUI.Services;
|
using MoonCoreUI.Services;
|
||||||
using Moonlight.Features.FileManager.Interfaces;
|
using Moonlight.Features.FileManager.Interfaces;
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
using Moonlight.Features.FileManager.UI.Components;
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Implementations;
|
namespace Moonlight.Features.FileManager.Implementations;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ public class RenameContextAction : IFileManagerContextAction
|
||||||
public string Color => "info";
|
public string Color => "info";
|
||||||
public Func<FileEntry, bool> Filter => _ => true;
|
public Func<FileEntry, bool> Filter => _ => true;
|
||||||
|
|
||||||
public async Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, FileEntry entry, IServiceProvider provider)
|
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider provider)
|
||||||
{
|
{
|
||||||
var alertService = provider.GetRequiredService<AlertService>();
|
var alertService = provider.GetRequiredService<AlertService>();
|
||||||
var toastService = provider.GetRequiredService<ToastService>();
|
var toastService = provider.GetRequiredService<ToastService>();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
using Moonlight.Features.FileManager.UI.Components;
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Interfaces;
|
namespace Moonlight.Features.FileManager.Interfaces;
|
||||||
|
|
||||||
|
@ -10,5 +10,5 @@ public interface IFileManagerContextAction
|
||||||
public string Color { get; }
|
public string Color { get; }
|
||||||
public Func<FileEntry, bool> Filter { get; }
|
public Func<FileEntry, bool> Filter { get; }
|
||||||
|
|
||||||
public Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, FileEntry entry, IServiceProvider provider);
|
public Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider provider);
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
using Moonlight.Features.FileManager.UI.Components;
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Interfaces;
|
namespace Moonlight.Features.FileManager.Interfaces;
|
||||||
|
|
||||||
|
@ -9,5 +9,5 @@ public interface IFileManagerCreateAction
|
||||||
public string Icon { get; }
|
public string Icon { get; }
|
||||||
public string Color { get; }
|
public string Color { get; }
|
||||||
|
|
||||||
public Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, IServiceProvider provider);
|
public Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, IServiceProvider provider);
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
using Moonlight.Features.FileManager.UI.NewFileManager;
|
using Moonlight.Features.FileManager.UI.Components;
|
||||||
|
|
||||||
namespace Moonlight.Features.FileManager.Interfaces;
|
namespace Moonlight.Features.FileManager.Interfaces;
|
||||||
|
|
||||||
|
@ -8,5 +8,5 @@ public interface IFileManagerSelectionAction
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public string Color { get; }
|
public string Color { get; }
|
||||||
|
|
||||||
public Task Execute(BaseFileAccess access, UI.NewFileManager.FileManager fileManager, FileEntry[] entries, IServiceProvider provider);
|
public Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry[] entries, IServiceProvider provider);
|
||||||
}
|
}
|
|
@ -50,7 +50,10 @@ public class BaseFileAccess : IDisposable
|
||||||
{
|
{
|
||||||
var finalPath = CurrentDirectory + entry.Name;
|
var finalPath = CurrentDirectory + entry.Name;
|
||||||
|
|
||||||
await Actions.Delete(finalPath);
|
if(entry.IsFile)
|
||||||
|
await Actions.DeleteFile(finalPath);
|
||||||
|
else
|
||||||
|
await Actions.DeleteDirectory(finalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Move(FileEntry entry, string to)
|
public async Task Move(FileEntry entry, string to)
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
public interface IFileActions : IDisposable
|
public interface IFileActions : IDisposable
|
||||||
{
|
{
|
||||||
public Task<FileEntry[]> List(string path);
|
public Task<FileEntry[]> List(string path);
|
||||||
public Task Delete(string path);
|
public Task DeleteFile(string path);
|
||||||
|
public Task DeleteDirectory(string path);
|
||||||
public Task Move(string from, string to);
|
public Task Move(string from, string to);
|
||||||
public Task CreateDirectory(string name);
|
public Task CreateDirectory(string path);
|
||||||
public Task CreateFile(string name);
|
public Task CreateFile(string path);
|
||||||
public Task<string> ReadFile(string name);
|
public Task<string> ReadFile(string path);
|
||||||
public Task WriteFile(string name, string content);
|
public Task WriteFile(string path, string content);
|
||||||
public Task<Stream> ReadFileStream(string name);
|
public Task<Stream> ReadFileStream(string path);
|
||||||
public Task WriteFileStream(string name, Stream dataStream);
|
public Task WriteFileStream(string path, Stream dataStream);
|
||||||
public IFileActions Clone();
|
public IFileActions Clone();
|
||||||
}
|
}
|
|
@ -1,282 +1,363 @@
|
||||||
@using Moonlight.Core.Configuration
|
|
||||||
@using MoonCore.Helpers
|
@using MoonCore.Helpers
|
||||||
@using MoonCore.Services
|
@using MoonCore.Services
|
||||||
@using MoonCoreUI.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.Models.Abstractions.FileAccess
|
||||||
|
@using Moonlight.Features.FileManager.Services
|
||||||
|
|
||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@inject ConfigService<CoreConfiguration> ConfigService
|
|
||||||
@inject ToastService ToastService
|
@inject ToastService ToastService
|
||||||
|
@inject FileManagerInteropService FileManagerInteropService
|
||||||
|
@inject SharedFileAccessService FileAccessService
|
||||||
|
@inject ConfigService<CoreConfiguration> ConfigService
|
||||||
|
@inject PluginService PluginService
|
||||||
|
@inject IServiceProvider ServiceProvider
|
||||||
|
|
||||||
<div class="card">
|
@implements IDisposable
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-title">
|
<div class="card card-body px-5">
|
||||||
<div class="badge badge-primary badge-lg fs-5 py-2">
|
<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 elements = Path
|
var parts = Path
|
||||||
.Split("/")
|
.Split("/")
|
||||||
.Where(x => !string.IsNullOrEmpty(x))
|
.Where(x => !string.IsNullOrEmpty(x))
|
||||||
.ToList();
|
.ToArray();
|
||||||
|
|
||||||
int i = 1;
|
var i = 1;
|
||||||
var root = "/";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<a href="#" @onclick:preventDefault @onclick="() => NavigateToPath(root)" class="invisible-a mx-2 text-white">/</a>
|
<a href="#" class="text-white mx-1" @onclick:preventDefault @onclick="() => NavigateBackToLevel(0)">/</a>
|
||||||
@foreach (var element in elements)
|
|
||||||
{
|
|
||||||
var pathToCd = "/" + string.Join('/', elements.Take(i));
|
|
||||||
|
|
||||||
<a href="#" @onclick:preventDefault @onclick="() => NavigateToPath(pathToCd)" class="invisible-a text-white">@(element)</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>
|
<div class="mx-2 text-white">/</div>
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-toolbar">
|
<div class="d-flex justify-content-center justify-content-md-end align-items-center">
|
||||||
@if (ShowFileUploader)
|
@if (View != null && View.Selection.Any())
|
||||||
{
|
{
|
||||||
<button type="button" @onclick="ToggleFileUploader" class="btn btn-light-primary me-3">
|
foreach (var action in SelectionActions)
|
||||||
Back
|
{
|
||||||
</button>
|
var cssClass = $"btn btn-{action.Color} mx-2";
|
||||||
|
|
||||||
|
<WButton Text="@action.Name" CssClasses="@cssClass" OnClick="() => InvokeSelectionAction(action)"/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<a href="javascript:void(0)" class="btn btn-secondary me-3">
|
<WButton OnClick="ManualRefresh" CssClasses="btn btn-icon btn-light-info">
|
||||||
<i class="bx bx-sm bx-link-external me-2"></i>
|
<i class="bx bx-sm bx-refresh"></i>
|
||||||
Launch
|
</WButton>
|
||||||
</a>
|
<label for="fileManagerSelect" class="btn btn-light-primary mx-2">Upload</label>
|
||||||
<button type="button" @onclick="ToggleFileUploader" class="btn btn-light-primary me-3">
|
<input id="fileManagerSelect" type="file" hidden="hidden" multiple/>
|
||||||
<i class="bx bx-sm bx-upload me-2"></i>
|
|
||||||
Upload
|
|
||||||
</button>
|
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a class="btn btn-primary dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="btn btn-primary dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
New
|
New
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
<ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||||
<li>
|
@foreach (var action in CreateActions)
|
||||||
<a href="#" @onclick:preventDefault @onclick="CreateFile" class="dropdown-item">
|
{
|
||||||
<i class="bx bx-sm bx-file text-primary me-2 align-middle"></i>
|
<li>
|
||||||
<span class="align-middle fs-6">File</span>
|
<a href="#" class="dropdown-item" @onclick:preventDefault @onclick="() => InvokeCreateAction(action)">
|
||||||
</a>
|
<i class="bx bx-sm @action.Icon text-@action.Color me-2 align-middle"></i>
|
||||||
</li>
|
<span class="align-middle fs-6">@action.Name</span>
|
||||||
<li>
|
</a>
|
||||||
<a href="#" @onclick:preventDefault @onclick="CreateDirectory" class="dropdown-item">
|
</li>
|
||||||
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body" @ondragenter="() => ToggleFileUploader(true)">
|
|
||||||
@if (ShowFileUploader)
|
|
||||||
{
|
|
||||||
<FileUploader @ref="FileUploader" FileAccess="FileAccess"/>
|
|
||||||
}
|
|
||||||
else if (ShowFileEditor)
|
|
||||||
{
|
|
||||||
<FileEditor File="EditorOpenFile" FileAccess="FileAccess" OnClosed="OnEditorClosed"/>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<FileView @ref="FileView"
|
|
||||||
FileAccess="FileAccess"
|
|
||||||
OnPathChanged="OnPathChanged"
|
|
||||||
OnFileClicked="OnFileClicked"
|
|
||||||
OnMoveRequested="StartMove"/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SmartModal @ref="MoveModal" CssClasses="modal-dialog-centered">
|
@if (ShowEditor)
|
||||||
<div class="modal-header">
|
{
|
||||||
<h5 class="modal-title">Select the location to move '@(MoveEntry.Name)'</h5>
|
<div class="card card-body px-2 py-2 mt-5">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<FileEditor @ref="Editor" FileAccess="FileAccess" File="FileToEdit" OnClosed="CloseEditor"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="overflow-y: scroll; max-height: 80vh">
|
}
|
||||||
<FileView
|
else
|
||||||
FileAccess="MoveAccess"
|
{
|
||||||
ShowActions="false"
|
<div id="fileManagerUpload" class="card card-body px-5 py-3 mt-5">
|
||||||
ShowHeader="false"
|
<FileView @ref="View"
|
||||||
ShowSelect="false"
|
FileAccess="FileAccess"
|
||||||
ShowSize="false"
|
OnEntryClicked="OnEntryClicked"
|
||||||
ShowLastModified="false"/>
|
OnNavigateUpClicked="OnNavigateUpClicked"
|
||||||
|
OnSelectionChanged="OnSelectionChanged"
|
||||||
|
EnableContextMenu="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>
|
</div>
|
||||||
<div class="modal-footer p-3">
|
|
||||||
<div class="btn-group w-100">
|
<SmartModal @ref="FolderSelectModal" CssClasses="modal-lg modal-dialog-centered">
|
||||||
<WButton OnClick="FinishMove" Text="Move" CssClasses="btn btn-primary w-50 me-3"/>
|
<div class="modal-header">
|
||||||
<button class="btn btn-secondary w-50" data-bs-dismiss="modal">Cancel</button>
|
<h5 class="modal-title">@FolderSelectTitle</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="HideFolderSelect"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="modal-body">
|
||||||
</SmartModal>
|
<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
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||||
|
|
||||||
// Navigation
|
public FileView View { get; private set; }
|
||||||
private string Path = "/";
|
private string Path = "/";
|
||||||
private FileView? FileView;
|
|
||||||
|
|
||||||
// Uploading
|
private IFileManagerContextAction[] ContextActions;
|
||||||
private bool ShowFileUploader = false;
|
private IFileManagerSelectionAction[] SelectionActions;
|
||||||
private FileUploader? FileUploader;
|
private IFileManagerCreateAction[] CreateActions;
|
||||||
|
|
||||||
// Editing
|
// Editor
|
||||||
private bool ShowFileEditor = false;
|
private FileEditor Editor;
|
||||||
private FileEntry EditorOpenFile;
|
private FileEntry FileToEdit;
|
||||||
|
private bool ShowEditor = false;
|
||||||
|
|
||||||
// Move
|
// Folder select dialog
|
||||||
private FileEntry MoveEntry;
|
private bool FolderSelectIsOpen = false;
|
||||||
private SmartModal MoveModal;
|
private SmartModal FolderSelectModal;
|
||||||
private BaseFileAccess MoveAccess;
|
private BaseFileAccess FolderSelectFileAccess;
|
||||||
|
private string FolderSelectTitle;
|
||||||
|
private Func<string, Task> FolderSelectResult;
|
||||||
|
private FileView FolderSelectView;
|
||||||
|
private Func<FileEntry, bool> FolderSelectFilter => entry => entry.IsDirectory;
|
||||||
|
|
||||||
private async Task OnPathChanged(string path)
|
private Timer? UploadTokenTimer;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Path = path;
|
// Load plugin ui and options
|
||||||
await InvokeAsync(StateHasChanged);
|
ContextActions = await PluginService.GetImplementations<IFileManagerContextAction>();
|
||||||
|
SelectionActions = await PluginService.GetImplementations<IFileManagerSelectionAction>();
|
||||||
|
CreateActions = await PluginService.GetImplementations<IFileManagerCreateAction>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task NavigateToPath(string path)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (ShowFileUploader)
|
if (!firstRender)
|
||||||
await ToggleFileUploader(false);
|
|
||||||
|
|
||||||
if (FileView == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await FileView.NavigateToPath(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Uploader
|
// Setup upload url update timer
|
||||||
|
UploadTokenTimer = new(async _ =>
|
||||||
private async Task ToggleFileUploader() => await ToggleFileUploader(!ShowFileUploader);
|
|
||||||
|
|
||||||
private async Task ToggleFileUploader(bool b)
|
|
||||||
{
|
|
||||||
ShowFileUploader = b;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region mkdir / touch
|
|
||||||
|
|
||||||
private async Task CreateFile()
|
|
||||||
{
|
|
||||||
if (FileView == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var name = await AlertService.Text("Enter the filename", "");
|
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(name))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (name.Contains(".."))
|
|
||||||
{
|
{
|
||||||
Logger.Warn($"Someone tried to use path transversal to create a file: '{name}'", "security");
|
await FileAccessService.Register(FileAccess);
|
||||||
return;
|
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.CreateFile(name);
|
|
||||||
|
|
||||||
await FileView.Refresh();
|
|
||||||
|
|
||||||
// Open editor to start editing
|
|
||||||
await OpenEditor(new FileEntry()
|
|
||||||
{
|
{
|
||||||
Size = 0,
|
await FileAccess.ChangeDirectory(entry.Name);
|
||||||
Name = name,
|
await View.Refresh();
|
||||||
IsFile = true,
|
|
||||||
IsDirectory = false,
|
|
||||||
LastModifiedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CreateDirectory()
|
await Refresh();
|
||||||
{
|
|
||||||
if (FileView == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var name = await AlertService.Text("Enter the folder name", "");
|
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(name))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (name.Contains(".."))
|
|
||||||
{
|
|
||||||
Logger.Warn($"Someone tried to use path transversal to create a file: '{name}'", "security");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await FileAccess.CreateDirectory(name);
|
|
||||||
|
|
||||||
await FileView.Refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
private async Task InvokeContextAction(IFileManagerContextAction contextAction, FileEntry entry)
|
||||||
|
|
||||||
#region Editor
|
|
||||||
|
|
||||||
private async Task OnFileClicked(FileEntry fileEntry) => await OpenEditor(fileEntry);
|
|
||||||
|
|
||||||
private async Task OpenEditor(FileEntry fileEntry)
|
|
||||||
{
|
{
|
||||||
var fileSizeInKilobytes = ByteSizeValue.FromBytes(fileEntry.Size).KiloBytes;
|
await View.HideContextMenu();
|
||||||
|
|
||||||
if (fileSizeInKilobytes > ConfigService.Get().Customisation.FileManager.MaxFileOpenSize)
|
await contextAction.Execute(FileAccess, this, entry, ServiceProvider);
|
||||||
{
|
}
|
||||||
await ToastService.Danger("Unable to open file as it exceeds the max file size limit");
|
|
||||||
|
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;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
EditorOpenFile = fileEntry;
|
var path = await FileAccess.GetCurrentDirectory();
|
||||||
|
|
||||||
// Prepare editor
|
var parts = path.Split("/");
|
||||||
ShowFileEditor = true;
|
var pathToNavigate = string.Join("/", parts.Take(level + 1)) + "/";
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
await FileAccess.SetDirectory(pathToNavigate);
|
||||||
|
await View.Refresh();
|
||||||
|
await Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnEditorClosed()
|
private async Task ManualRefresh()
|
||||||
{
|
{
|
||||||
ShowFileEditor = false;
|
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);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Move
|
#region File Editor
|
||||||
|
|
||||||
private async Task StartMove(FileEntry fileEntry)
|
public async Task OpenEditor(FileEntry entry)
|
||||||
{
|
{
|
||||||
MoveEntry = fileEntry;
|
FileToEdit = entry;
|
||||||
MoveAccess = FileAccess.Clone();
|
ShowEditor = true;
|
||||||
|
|
||||||
await MoveAccess.SetDirectory("/");
|
await InvokeAsync(StateHasChanged);
|
||||||
await MoveModal.Show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FinishMove()
|
public async Task CloseEditor()
|
||||||
{
|
{
|
||||||
var pathToMove = await MoveAccess.GetCurrentDirectory();
|
ShowEditor = false;
|
||||||
MoveAccess.Dispose();
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
// Perform move and process ui updates
|
|
||||||
await FileAccess.Move(MoveEntry, pathToMove + MoveEntry.Name);
|
|
||||||
|
|
||||||
await MoveModal.Hide();
|
|
||||||
|
|
||||||
if (FileView == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await FileView.Refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
@using Moonlight.Core.Services
|
|
||||||
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
|
|
||||||
@using Moonlight.Features.FileManager.Services
|
|
||||||
|
|
||||||
@inject DropzoneService DropzoneService
|
|
||||||
@inject SharedFileAccessService SharedFileAccessService
|
|
||||||
|
|
||||||
@implements IDisposable
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<div class="col-md-8" @ondrop:preventDefault>
|
|
||||||
<div class="dropzone dropzone-queue" id="@DropzoneId">
|
|
||||||
<div class="dropzone-panel mb-lg-0 mb-2">
|
|
||||||
<div class="card border border-1 border-primary bg-secondary" style="pointer-events: none">
|
|
||||||
<div class="card-body">
|
|
||||||
<IconAlert Title="" Color="primary" Icon="bx-cloud-upload">
|
|
||||||
<div class="text-center fs-1 fw-bold">
|
|
||||||
Drag a file or folder or <a class="dropzone-select" style="pointer-events: all">click to upload files</a>
|
|
||||||
</div>
|
|
||||||
</IconAlert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropzone-items wm-200px">
|
|
||||||
<div class="dropzone-item" style="display:none">
|
|
||||||
<div class="dropzone-file">
|
|
||||||
<div class="dropzone-filename" title="some_image_file_name.jpg">
|
|
||||||
<span data-dz-name>some_image_file_name.jpg</span>
|
|
||||||
<strong>(<span data-dz-size>340kb</span>)</strong>
|
|
||||||
</div>
|
|
||||||
<div class="dropzone-error" data-dz-errormessage></div>
|
|
||||||
</div>
|
|
||||||
<div class="dropzone-progress">
|
|
||||||
<div class="progress">
|
|
||||||
<div
|
|
||||||
class="progress-bar bg-primary"
|
|
||||||
role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" data-dz-uploadprogress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropzone-toolbar">
|
|
||||||
<span class="dropzone-delete" data-dz-remove>
|
|
||||||
<i class="bx bx-x fs-1"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
|
||||||
|
|
||||||
private CancellationTokenSource Cancellation = new();
|
|
||||||
private string DropzoneId;
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
DropzoneId = $"dropzone{GetHashCode()}";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
//await SharedFileAccessService.Register(FileAccess);
|
|
||||||
|
|
||||||
//var token = await SharedFileAccessService.GenerateToken(FileAccess);
|
|
||||||
//var url = $"/api/upload?token={token}";
|
|
||||||
|
|
||||||
//await DropzoneService.Create(DropzoneId, url);
|
|
||||||
|
|
||||||
Task.Run(async () => // Update the dropzone url every 5 minutes so the token does not expire
|
|
||||||
{
|
|
||||||
while (!Cancellation.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromMinutes(5));
|
|
||||||
|
|
||||||
//var newToken = await SharedFileAccessService.GenerateToken(FileAccess);
|
|
||||||
//var newUrl = $"/api/upload?token={newToken}";
|
|
||||||
//await DropzoneService.UpdateUrl(DropzoneId, newUrl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void Dispose()
|
|
||||||
{
|
|
||||||
Cancellation.Cancel();
|
|
||||||
//await SharedFileAccessService.Unregister(FileAccess);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,247 +1,287 @@
|
||||||
@using MoonCoreUI.Services
|
|
||||||
@using MoonCore.Helpers
|
@using MoonCore.Helpers
|
||||||
@using Moonlight.Core.Services
|
|
||||||
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
|
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
|
||||||
@using Moonlight.Features.FileManager.Services
|
@using BlazorContextMenu
|
||||||
|
|
||||||
@inject ToastService ToastService
|
@inject IJSRuntime JsRuntime
|
||||||
@inject AlertService AlertService
|
|
||||||
@inject SharedFileAccessService SharedFileAccessService
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
@implements IDisposable
|
<div class="@(IsLoading ? "table-loading" : "")">
|
||||||
|
@if (IsLoading)
|
||||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
{
|
||||||
<table class="w-100 table table-responsive table-row-bordered">
|
<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>
|
<tbody>
|
||||||
|
<tr class="text-muted">
|
||||||
@if (ShowHeader)
|
@if (ShowSelect)
|
||||||
{
|
{
|
||||||
<tr>
|
<td class="w-10px align-middle">
|
||||||
@if (ShowSelect)
|
<div class="form-check">
|
||||||
{
|
@if (IsAllSelected)
|
||||||
<td class="w-10px align-middle">
|
{
|
||||||
<div class="form-check">
|
<input class="form-check-input" type="checkbox" value="1" checked="checked" @oninput="() => ChangeAllSelection(false)">
|
||||||
<input class="form-check-input" type="checkbox" @oninput="args => ToggleAll(args)">
|
}
|
||||||
</div>
|
else
|
||||||
</td>
|
{
|
||||||
}
|
<input class="form-check-input" type="checkbox" value="0" @oninput="() => ChangeAllSelection(true)">
|
||||||
@if (ShowIcons)
|
}
|
||||||
{
|
</div>
|
||||||
<td></td>
|
|
||||||
}
|
|
||||||
<td class="align-middle fs-6 text-muted">
|
|
||||||
Name
|
|
||||||
</td>
|
</td>
|
||||||
@if (ShowSize)
|
}
|
||||||
{
|
<td class="w-10px"></td>
|
||||||
<td class="align-middle fs-6 text-muted d-none d-sm-table-cell text-end">
|
<td>Name</td>
|
||||||
Size
|
@if (ShowSize)
|
||||||
</td>
|
{
|
||||||
}
|
<td class="d-none d-md-table-cell">Size</td>
|
||||||
@if (ShowLastModified)
|
}
|
||||||
{
|
@if (ShowDate)
|
||||||
<td class="align-middle fs-6 text-muted d-none d-sm-table-cell text-end">
|
{
|
||||||
Last modified at
|
<td class="d-none d-md-table-cell">Last modified</td>
|
||||||
</td>
|
}
|
||||||
}
|
@if (EnableContextMenu)
|
||||||
@if (SelectedEntries.Count == 0)
|
{
|
||||||
{
|
<td></td>
|
||||||
<td></td>
|
}
|
||||||
}
|
@if (AdditionTemplate != null)
|
||||||
else
|
{
|
||||||
{
|
<td></td>
|
||||||
<td class="w-50 fs-6 text-end">
|
}
|
||||||
<span class="text-primary">@SelectedEntries.Count</span> element(s) selected
|
</tr>
|
||||||
<div class="ms-2 btn-group">
|
|
||||||
<WButton OnClick="() => Delete(SelectedEntries.ToArray())" CssClasses="btn btn-icon btn-danger">
|
|
||||||
<i class="text-white bx bx-sm bx-trash"></i>
|
|
||||||
</WButton>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (ShowGoUp && Path != "/" && !DisableNavigation)
|
@if (Path != "/" && ShowNavigateUp)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr class="fw-semibold">
|
||||||
@if (ShowSelect)
|
@if (ShowSelect)
|
||||||
{
|
{
|
||||||
<td class="w-10px align-middle">
|
<td class="align-middle w-10px"></td>
|
||||||
</td>
|
|
||||||
}
|
}
|
||||||
@if (ShowIcons)
|
<td class="w-10px">
|
||||||
{
|
<i class="bx bx-sm bx-chevrons-left"></i>
|
||||||
<td class="w-10px align-middle">
|
</td>
|
||||||
</td>
|
<td>
|
||||||
}
|
<a href="#" @onclick:preventDefault @onclick="NavigateUp">
|
||||||
<td class="align-middle fs-6">
|
Back to parent folder
|
||||||
@{
|
|
||||||
var upPath = "..";
|
|
||||||
}
|
|
||||||
|
|
||||||
<a href="#"
|
|
||||||
@onclick:preventDefault
|
|
||||||
@onclick="() => Navigate(upPath)">
|
|
||||||
Go up
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@if (ShowSize)
|
@if (ShowSize)
|
||||||
{
|
{
|
||||||
<td class="align-middle fs-6 d-none d-sm-table-cell text-end">
|
<td></td>
|
||||||
<span>-</span>
|
|
||||||
</td>
|
|
||||||
}
|
}
|
||||||
@if (ShowLastModified)
|
@if (ShowDate)
|
||||||
{
|
{
|
||||||
<td class="align-middle fs-6 d-none d-sm-table-cell text-end">
|
<td></td>
|
||||||
-
|
|
||||||
</td>
|
|
||||||
}
|
}
|
||||||
@if (ShowActions)
|
@if (EnableContextMenu)
|
||||||
{
|
{
|
||||||
<td class="w-50 text-end">
|
<td></td>
|
||||||
</td>
|
}
|
||||||
|
@if (AdditionTemplate != null)
|
||||||
|
{
|
||||||
|
<td></td>
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|
||||||
@foreach (var entry in Entries)
|
@foreach (var entry in Entries)
|
||||||
{
|
{
|
||||||
<tr>
|
if (EnableContextMenu)
|
||||||
@if (ShowSelect)
|
{
|
||||||
{
|
<ContextMenuTrigger MenuId="@ContextMenuId" WrapperTag="tr" Data="entry">
|
||||||
<td class="w-10px align-middle">
|
@if (ShowSelect)
|
||||||
<div class="form-check">
|
{
|
||||||
@if (SelectedEntries.Contains(entry))
|
<td class="w-10px align-middle">
|
||||||
{
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" value="1" checked="checked" @oninput="args => HandleSelected(entry, args)">
|
@if (SelectionCache.ContainsKey(entry) && SelectionCache[entry])
|
||||||
}
|
{
|
||||||
else
|
<input class="form-check-input" type="checkbox" value="1" checked="checked" @oninput="() => ChangeSelection(entry, false)">
|
||||||
{
|
}
|
||||||
<input class="form-check-input" type="checkbox" value="0" @oninput="args => HandleSelected(entry, args)">
|
else
|
||||||
}
|
{
|
||||||
</div>
|
<input class="form-check-input" type="checkbox" value="0" @oninput="() => ChangeSelection(entry, true)">
|
||||||
</td>
|
}
|
||||||
}
|
</div>
|
||||||
@if (ShowIcons)
|
</td>
|
||||||
{
|
}
|
||||||
<td class="w-10px align-middle">
|
<td class="align-middle w-10px">
|
||||||
@if (entry.IsFile)
|
@if (entry.IsFile)
|
||||||
{
|
{
|
||||||
<i class="bx bx-md bx-file"></i>
|
<i class="bx bx-md bxs-file-blank text-white"></i>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<i class="bx bx-md bx-folder"></i>
|
<i class="bx bx-md bxs-folder text-primary"></i>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
<td class="align-middle">
|
||||||
<td class="align-middle fs-6">
|
<a href="#" @onclick:preventDefault @onclick="() => HandleEntryClick(entry)">
|
||||||
@if (DisableNavigation)
|
@entry.Name
|
||||||
{
|
|
||||||
<span>@(entry.Name)</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a href="#"
|
|
||||||
@onclick:preventDefault
|
|
||||||
@onclick="() => HandleClick(entry)">
|
|
||||||
@(entry.Name)
|
|
||||||
</a>
|
</a>
|
||||||
|
</td>
|
||||||
|
@if (ShowSize)
|
||||||
|
{
|
||||||
|
<td class="align-middle d-none d-md-table-cell">
|
||||||
|
@if (entry.IsFile)
|
||||||
|
{
|
||||||
|
@Formatter.FormatSize(entry.Size)
|
||||||
|
}
|
||||||
|
</td>
|
||||||
}
|
}
|
||||||
</td>
|
@if (ShowDate)
|
||||||
@if (ShowSize)
|
{
|
||||||
{
|
<td class="align-middle d-none d-md-table-cell">
|
||||||
<td class="align-middle fs-6 d-none d-sm-table-cell text-end">
|
@Formatter.FormatDate(entry.LastModifiedAt)
|
||||||
@if (entry.IsFile)
|
</td>
|
||||||
{
|
}
|
||||||
@(Formatter.FormatSize(entry.Size))
|
<td class="d-table-cell d-md-none">
|
||||||
}
|
<div class="dropstart">
|
||||||
else
|
<button class="btn btn-icon btn-secondary" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
{
|
<i class="bx bx-sm bx-dots-horizontal"></i>
|
||||||
<span>-</span>
|
</button>
|
||||||
}
|
<div class="dropdown-menu fs-6">
|
||||||
</td>
|
@if (ContextMenuTemplate != null)
|
||||||
}
|
{
|
||||||
@if (ShowLastModified)
|
@ContextMenuTemplate.Invoke(entry)
|
||||||
{
|
}
|
||||||
<td class="align-middle fs-6 d-none d-sm-table-cell text-end">
|
|
||||||
@(Formatter.FormatDate(entry.LastModifiedAt))
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
@if (ShowActions)
|
|
||||||
{
|
|
||||||
<td class="w-50 text-end">
|
|
||||||
<div class="btn-group">
|
|
||||||
<WButton OnClick="() => Delete(entry)" CssClasses="btn btn-icon btn-danger">
|
|
||||||
<i class="text-white bx bx-sm bx-trash"></i>
|
|
||||||
</WButton>
|
|
||||||
|
|
||||||
<div class="dropdown">
|
|
||||||
<button class="btn btn-icon btn-secondary rounded-start-0" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
<i class="text-white bx bx-sm bx-dots-horizontal-rounded"></i>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<a href="#" @onclick:preventDefault @onclick="() => Rename(entry)" class="dropdown-item">Rename</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" @onclick:preventDefault @onclick="() => Download(entry)" class="dropdown-item">Download</a>
|
|
||||||
</li>
|
|
||||||
@if (OnMoveRequested != null)
|
|
||||||
{
|
|
||||||
<li>
|
|
||||||
<a href="#" @onclick:preventDefault @onclick="() => RequestMove(entry)" class="dropdown-item">Move</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
}
|
@if (AdditionTemplate != null)
|
||||||
</tr>
|
{
|
||||||
|
@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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</LazyLoader>
|
</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
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
[Parameter] public RenderFragment<FileEntry>? AdditionTemplate { get; set; }
|
||||||
|
|
||||||
[Parameter] public Func<FileEntry, bool>? Filter { get; set; }
|
|
||||||
[Parameter] public bool ShowSize { get; set; } = true;
|
[Parameter] public bool ShowSize { get; set; } = true;
|
||||||
[Parameter] public bool ShowLastModified { get; set; } = true;
|
[Parameter] public bool ShowDate { get; set; } = true;
|
||||||
[Parameter] public bool ShowIcons { get; set; } = true;
|
|
||||||
[Parameter] public bool ShowActions { get; set; } = true;
|
|
||||||
[Parameter] public bool ShowSelect { get; set; } = true;
|
[Parameter] public bool ShowSelect { get; set; } = true;
|
||||||
[Parameter] public bool ShowGoUp { get; set; } = true;
|
[Parameter] public bool ShowNavigateUp { get; set; } = true;
|
||||||
[Parameter] public bool ShowHeader { get; set; } = true;
|
|
||||||
[Parameter] public bool DisableNavigation { get; set; } = false;
|
|
||||||
[Parameter] public Func<FileEntry, Task>? OnFileClicked { get; set; }
|
|
||||||
[Parameter] public Func<Task>? OnSelectionChanged { get; set; }
|
|
||||||
[Parameter] public Func<string, Task>? OnPathChanged { get; set; }
|
|
||||||
[Parameter] public Func<FileEntry, Task>? OnMoveRequested { get; set; }
|
|
||||||
|
|
||||||
public readonly List<FileEntry> SelectedEntries = new();
|
[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;
|
||||||
|
|
||||||
private LazyLoader LazyLoader;
|
[Parameter] public BaseFileAccess FileAccess { get; set; }
|
||||||
private FileEntry[] Entries;
|
[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 string Path = "/";
|
||||||
|
|
||||||
private async Task Load(LazyLoader lazyLoader)
|
private Dictionary<FileEntry, bool> SelectionCache = new();
|
||||||
{
|
public FileEntry[] Selection => SelectionCache.Where(x => x.Value).Select(x => x.Key).ToArray();
|
||||||
await lazyLoader.SetText("Loading files and folders");
|
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);
|
||||||
|
|
||||||
// Load all entries
|
|
||||||
Entries = await FileAccess.List();
|
Entries = await FileAccess.List();
|
||||||
|
|
||||||
await lazyLoader.SetText("Sorting files and folders");
|
// Sort entries
|
||||||
|
LoadingText = "Sorting files and folders";
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
// Perform sorting and filtering
|
|
||||||
if (Filter != null)
|
if (Filter != null)
|
||||||
{
|
{
|
||||||
Entries = Entries
|
Entries = Entries
|
||||||
|
@ -255,180 +295,93 @@
|
||||||
.SelectMany(x => x.OrderBy(y => y.Name))
|
.SelectMany(x => x.OrderBy(y => y.Name))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
SelectedEntries.Clear();
|
// Build selection cache
|
||||||
|
SelectionCache.Clear();
|
||||||
|
|
||||||
Path = await FileAccess.GetCurrentDirectory();
|
foreach (var entry in Entries)
|
||||||
|
SelectionCache.Add(entry, false);
|
||||||
|
|
||||||
if (OnPathChanged != null)
|
if (OnSelectionChanged != null)
|
||||||
await OnPathChanged.Invoke(Path);
|
await OnSelectionChanged.Invoke(Array.Empty<FileEntry>());
|
||||||
|
|
||||||
|
IsLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleClick(FileEntry fileEntry)
|
private async Task HandleEntryClick(FileEntry entry)
|
||||||
{
|
{
|
||||||
if (fileEntry.IsDirectory && !DisableNavigation)
|
if (OnEntryClicked == null)
|
||||||
{
|
|
||||||
await Navigate(fileEntry.Name);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (OnFileClicked != null)
|
|
||||||
await OnFileClicked.Invoke(fileEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Actions
|
|
||||||
|
|
||||||
private async Task Delete(params FileEntry[] entries)
|
|
||||||
{
|
|
||||||
if (entries.Length == 0)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var fileNameDesc = entries.Length == 1 ? entries.First().Name : $"{entries.Length} files";
|
await OnEntryClicked.Invoke(entry);
|
||||||
var confirm = await AlertService.YesNo($"Do you really want to delete '{fileNameDesc}'?", "Yes", "No");
|
|
||||||
|
|
||||||
if(!confirm)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var toastId = "fileDelete" + GetHashCode();
|
|
||||||
await ToastService.CreateProgress(toastId, $"[0/{entries.Length}] Deleting items");
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
await ToastService.ModifyProgress(toastId, $"[{i + 1}/{entries.Length}] Deleting '{entry.Name}'");
|
|
||||||
|
|
||||||
await FileAccess.Delete(entry);
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ToastService.RemoveProgress(toastId);
|
|
||||||
await ToastService.Success($"Successfully deleted {i} item(s)");
|
|
||||||
|
|
||||||
await LazyLoader.Reload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Rename(FileEntry fileEntry)
|
private async Task NavigateUp()
|
||||||
{
|
{
|
||||||
var name = await AlertService.Text($"Rename '{fileEntry.Name}'", "", fileEntry.Name);
|
if (OnNavigateUpClicked == null)
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(name))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await FileAccess.Move(fileEntry, await FileAccess.GetCurrentDirectory() + name);
|
await OnNavigateUpClicked.Invoke();
|
||||||
|
|
||||||
await LazyLoader.Reload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RequestMove(FileEntry fileEntry)
|
|
||||||
{
|
|
||||||
if (OnMoveRequested == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await OnMoveRequested.Invoke(fileEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Download(FileEntry fileEntry)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//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);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.Warn("Unable to start download");
|
|
||||||
Logger.Warn(e);
|
|
||||||
|
|
||||||
await ToastService.Danger("Failed to start download");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Selection
|
#region Selection
|
||||||
|
|
||||||
private async Task HandleSelected(FileEntry fileEntry, ChangeEventArgs args)
|
private async Task ChangeSelection(FileEntry entry, bool selectionState)
|
||||||
{
|
{
|
||||||
if (args.Value == null) // This should never be called. Still i want to handle it
|
SelectionCache[entry] = selectionState;
|
||||||
return;
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
if (args.Value.ToString() == "True")
|
|
||||||
{
|
|
||||||
if (!SelectedEntries.Contains(fileEntry))
|
|
||||||
SelectedEntries.Add(fileEntry);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (SelectedEntries.Contains(fileEntry))
|
|
||||||
SelectedEntries.Remove(fileEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OnSelectionChanged != null)
|
if (OnSelectionChanged != null)
|
||||||
await OnSelectionChanged.Invoke();
|
{
|
||||||
|
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);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ToggleAll(ChangeEventArgs args)
|
private async Task OnContextMenuHide()
|
||||||
{
|
{
|
||||||
if (args.Value == null)
|
ShowContextMenu = false;
|
||||||
return;
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
if (args.Value.ToString() == "True")
|
|
||||||
{
|
|
||||||
foreach (var entry in Entries)
|
|
||||||
{
|
|
||||||
if (!SelectedEntries.Contains(entry))
|
|
||||||
SelectedEntries.Add(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SelectedEntries.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public async Task HideContextMenu()
|
||||||
|
{
|
||||||
|
ShowContextMenu = false;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Navigation
|
|
||||||
|
|
||||||
public async Task Navigate(string name)
|
|
||||||
{
|
|
||||||
await LazyLoader.Reload(async loader =>
|
|
||||||
{
|
|
||||||
await loader.SetText("Switching directory on target");
|
|
||||||
await FileAccess.ChangeDirectory(name);
|
|
||||||
|
|
||||||
if (OnPathChanged != null)
|
|
||||||
await OnPathChanged.Invoke(await FileAccess.GetCurrentDirectory());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task NavigateToPath(string path)
|
|
||||||
{
|
|
||||||
await LazyLoader.Reload(async loader =>
|
|
||||||
{
|
|
||||||
await loader.SetText("Switching directory on target");
|
|
||||||
await FileAccess.SetDirectory(path);
|
|
||||||
|
|
||||||
if (OnPathChanged != null)
|
|
||||||
await OnPathChanged.Invoke(await FileAccess.GetCurrentDirectory());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public async Task Refresh() => await LazyLoader.Reload();
|
|
||||||
|
|
||||||
public async void Dispose()
|
|
||||||
{
|
|
||||||
//await SharedFileAccessService.Unregister(FileAccess);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,60 +0,0 @@
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,364 +0,0 @@
|
||||||
@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">
|
|
||||||
@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">
|
|
||||||
<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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,387 +0,0 @@
|
||||||
@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
|
|
||||||
|
|
||||||
}
|
|
49
Moonlight/Features/Servers/Helpers/ServerApiFileActions.cs
Normal file
49
Moonlight/Features/Servers/Helpers/ServerApiFileActions.cs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
||||||
|
using Moonlight.Features.Servers.Exceptions;
|
||||||
|
|
||||||
|
namespace Moonlight.Features.Servers.Helpers;
|
||||||
|
|
||||||
|
public class ServerApiFileActions : IFileActions
|
||||||
|
{
|
||||||
|
private readonly string Endpoint;
|
||||||
|
private readonly string Token;
|
||||||
|
private readonly int ServerId;
|
||||||
|
|
||||||
|
private readonly HttpApiClient<NodeException> ApiClient;
|
||||||
|
|
||||||
|
public ServerApiFileActions(string endpoint, string token, int serverId)
|
||||||
|
{
|
||||||
|
Endpoint = endpoint;
|
||||||
|
Token = token;
|
||||||
|
ServerId = serverId;
|
||||||
|
|
||||||
|
ApiClient = new(endpoint + $"files/{ServerId}/", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FileEntry[]> List(string path) => await ApiClient.Get<FileEntry[]>($"list?path={path}");
|
||||||
|
|
||||||
|
public async Task DeleteFile(string path) => await ApiClient.DeleteAsString($"deleteFile?path={path}");
|
||||||
|
|
||||||
|
public async Task DeleteDirectory(string path) => await ApiClient.DeleteAsString($"deleteDirectory?path={path}");
|
||||||
|
|
||||||
|
public async Task Move(string from, string to) => await ApiClient.Post($"move?from={from}&to={to}");
|
||||||
|
|
||||||
|
public async Task CreateDirectory(string path) => await ApiClient.Post($"createDirectory?path={path}");
|
||||||
|
|
||||||
|
public async Task CreateFile(string path) => await ApiClient.Post($"createFile?path={path}");
|
||||||
|
|
||||||
|
public async Task<string> ReadFile(string path) => await ApiClient.GetAsString($"readFile?path={path}");
|
||||||
|
|
||||||
|
public async Task WriteFile(string path, string content) =>
|
||||||
|
await ApiClient.PostAsString($"writeFile?path={path}", content);
|
||||||
|
|
||||||
|
public async Task<Stream> ReadFileStream(string path) => await ApiClient.GetAsStream($"readFileStream?path={path}");
|
||||||
|
|
||||||
|
public async Task WriteFileStream(string path, Stream dataStream) =>
|
||||||
|
await ApiClient.PostFile($"writeFileStream?path={path}", dataStream, "upload");
|
||||||
|
|
||||||
|
public IFileActions Clone() => new ServerApiFileActions(Endpoint, Token, ServerId);
|
||||||
|
|
||||||
|
public void Dispose() => ApiClient.Dispose();
|
||||||
|
}
|
|
@ -1,224 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using System.Text;
|
|
||||||
using FluentFTP;
|
|
||||||
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
|
|
||||||
|
|
||||||
namespace Moonlight.Features.Servers.Helpers;
|
|
||||||
|
|
||||||
public class ServerFtpFileActions : IFileActions
|
|
||||||
{
|
|
||||||
private FtpClient Client;
|
|
||||||
|
|
||||||
private readonly string Host;
|
|
||||||
private readonly int Port;
|
|
||||||
private readonly string Username;
|
|
||||||
private readonly string Password;
|
|
||||||
private readonly int OperationTimeout;
|
|
||||||
|
|
||||||
public ServerFtpFileActions(string host, int port, string username, string password, int operationTimeout = 5)
|
|
||||||
{
|
|
||||||
Host = host;
|
|
||||||
Port = port;
|
|
||||||
Username = username;
|
|
||||||
Password = password;
|
|
||||||
OperationTimeout = (int)TimeSpan.FromSeconds(5).TotalMilliseconds;
|
|
||||||
|
|
||||||
Client = CreateClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FileEntry[]> List(string path)
|
|
||||||
{
|
|
||||||
return await ExecuteHandled(() =>
|
|
||||||
{
|
|
||||||
var items = Client.GetListing(path) ?? Array.Empty<FtpListItem>();
|
|
||||||
var result = items.Select(item => new FileEntry
|
|
||||||
{
|
|
||||||
Name = item.Name,
|
|
||||||
IsDirectory = item.Type == FtpObjectType.Directory,
|
|
||||||
IsFile = item.Type == FtpObjectType.File,
|
|
||||||
LastModifiedAt = item.Modified,
|
|
||||||
Size = item.Size
|
|
||||||
}).ToArray();
|
|
||||||
|
|
||||||
return Task.FromResult(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Delete(string path)
|
|
||||||
{
|
|
||||||
await ExecuteHandled(() =>
|
|
||||||
{
|
|
||||||
if (Client.FileExists(path))
|
|
||||||
Client.DeleteFile(path);
|
|
||||||
else
|
|
||||||
Client.DeleteDirectory(path);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Move(string from, string to)
|
|
||||||
{
|
|
||||||
await ExecuteHandled(() =>
|
|
||||||
{
|
|
||||||
var fromEntry = Client.GetObjectInfo(from);
|
|
||||||
|
|
||||||
string dest;
|
|
||||||
|
|
||||||
if (to.EndsWith("/"))
|
|
||||||
dest = to + Path.GetFileName(from);
|
|
||||||
else
|
|
||||||
dest = to;
|
|
||||||
|
|
||||||
//from = CurrentDirectory + from;
|
|
||||||
//var fromWithSlash = from.StartsWith("/") ? from : "/" + from;
|
|
||||||
|
|
||||||
if (from == dest)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (fromEntry.Type == FtpObjectType.Directory)
|
|
||||||
// We need to add the folder name here, because some ftp servers would refuse to move the folder if its missing
|
|
||||||
Client.MoveDirectory(from, dest);
|
|
||||||
else
|
|
||||||
// We need to add the file name here, because some ftp servers would refuse to move the file if its missing
|
|
||||||
Client.MoveFile(from, dest);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateDirectory(string name)
|
|
||||||
{
|
|
||||||
await ExecuteHandled(() =>
|
|
||||||
{
|
|
||||||
Client.CreateDirectory(name);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateFile(string name)
|
|
||||||
{
|
|
||||||
await ExecuteHandled(() =>
|
|
||||||
{
|
|
||||||
using var stream = new MemoryStream();
|
|
||||||
Client.UploadStream(stream, name);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> ReadFile(string name)
|
|
||||||
{
|
|
||||||
return await ExecuteHandled(async () =>
|
|
||||||
{
|
|
||||||
await using var stream = Client.OpenRead(name);
|
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
|
||||||
return await reader.ReadToEndAsync();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WriteFile(string name, string content)
|
|
||||||
{
|
|
||||||
await ExecuteHandled(() =>
|
|
||||||
{
|
|
||||||
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
|
||||||
Client.UploadStream(stream, name);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Stream> ReadFileStream(string name)
|
|
||||||
{
|
|
||||||
return await ExecuteHandled(() =>
|
|
||||||
{
|
|
||||||
var stream = Client.OpenRead(name);
|
|
||||||
return Task.FromResult(stream);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WriteFileStream(string name, Stream dataStream)
|
|
||||||
{
|
|
||||||
await ExecuteHandled(() =>
|
|
||||||
{
|
|
||||||
Client.UploadStream(dataStream, name, FtpRemoteExists.Overwrite);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public IFileActions Clone()
|
|
||||||
{
|
|
||||||
return new ServerFtpFileActions(Host, Port, Username, Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Client.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Helpers
|
|
||||||
|
|
||||||
private Task EnsureConnected()
|
|
||||||
{
|
|
||||||
if (!Client.IsConnected)
|
|
||||||
Client.Connect();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExecuteHandled(Func<Task> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await EnsureConnected();
|
|
||||||
await func.Invoke();
|
|
||||||
}
|
|
||||||
catch (TimeoutException)
|
|
||||||
{
|
|
||||||
Client.Dispose();
|
|
||||||
Client = CreateClient();
|
|
||||||
|
|
||||||
await EnsureConnected();
|
|
||||||
|
|
||||||
await func.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T> ExecuteHandled<T>(Func<Task<T>> func)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await EnsureConnected();
|
|
||||||
return await func.Invoke();
|
|
||||||
}
|
|
||||||
catch (TimeoutException)
|
|
||||||
{
|
|
||||||
Client.Dispose();
|
|
||||||
Client = CreateClient();
|
|
||||||
|
|
||||||
await EnsureConnected();
|
|
||||||
|
|
||||||
return await func.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private FtpClient CreateClient()
|
|
||||||
{
|
|
||||||
var client = new FtpClient();
|
|
||||||
client.Host = Host;
|
|
||||||
client.Port = Port;
|
|
||||||
client.Credentials = new NetworkCredential(Username, Password);
|
|
||||||
client.Config.DataConnectionType = FtpDataConnectionType.AutoPassive;
|
|
||||||
|
|
||||||
client.Config.ConnectTimeout = OperationTimeout;
|
|
||||||
client.Config.ReadTimeout = OperationTimeout;
|
|
||||||
client.Config.DataConnectionConnectTimeout = OperationTimeout;
|
|
||||||
client.Config.DataConnectionReadTimeout = OperationTimeout;
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
|
@ -208,12 +208,10 @@ public class ServerService
|
||||||
await Backup.Delete(serverWithBackups, backup, false);
|
await Backup.Delete(serverWithBackups, backup, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BaseFileAccess> OpenFileAccess(Server s)
|
public Task<BaseFileAccess> OpenFileAccess(Server s)
|
||||||
{
|
{
|
||||||
using var scope = ServiceProvider.CreateScope();
|
using var scope = ServiceProvider.CreateScope();
|
||||||
|
|
||||||
var jwtService = ServiceProvider.GetRequiredService<JwtService<ServersJwtType>>();
|
|
||||||
var configService = ServiceProvider.GetRequiredService<ConfigService<CoreConfiguration>>();
|
|
||||||
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
|
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
|
||||||
|
|
||||||
var server = serverRepo
|
var server = serverRepo
|
||||||
|
@ -221,18 +219,14 @@ public class ServerService
|
||||||
.Include(x => x.Node)
|
.Include(x => x.Node)
|
||||||
.First(x => x.Id == s.Id);
|
.First(x => x.Id == s.Id);
|
||||||
|
|
||||||
var ftpLoginJwt = await jwtService.Create(data => { data.Add("ServerId", s.Id.ToString()); },
|
var protocol = server.Node.Ssl ? "https" : "http";
|
||||||
ServersJwtType.FtpServerLogin, TimeSpan.FromMinutes(5));
|
var remoteUrl = $"{protocol}://{server.Node.Fqdn}:{server.Node.HttpPort}/";
|
||||||
|
|
||||||
return new BaseFileAccess(
|
var result = new BaseFileAccess(
|
||||||
new ServerFtpFileActions(
|
new ServerApiFileActions(remoteUrl, server.Node.Token, server.Id)
|
||||||
server.Node.Fqdn,
|
|
||||||
server.Node.FtpPort,
|
|
||||||
$"moonlight.{server.Id}",
|
|
||||||
ftpLoginJwt,
|
|
||||||
configService.Get().Customisation.FileManager.OperationTimeout
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ServerListItem[]> GetServersList(ServerNode node, bool includeOffline = false)
|
public async Task<ServerListItem[]> GetServersList(ServerNode node, bool includeOffline = false)
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
@page "/test"
|
|
||||||
|
|
||||||
@using Moonlight.Features.FileManager.UI.NewFileManager
|
|
||||||
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
|
|
||||||
@using Moonlight.Core.Helpers
|
|
||||||
|
|
||||||
<FileManager FileAccess="FileAccess" />
|
|
||||||
|
|
||||||
@code
|
|
||||||
{
|
|
||||||
private BaseFileAccess FileAccess = new(new HostFileActions("storage"));
|
|
||||||
}
|
|
|
@ -85,13 +85,12 @@
|
||||||
<PackageReference Include="Blazor-ApexCharts" Version="2.3.3" />
|
<PackageReference Include="Blazor-ApexCharts" Version="2.3.3" />
|
||||||
<PackageReference Include="Blazor.ContextMenu" Version="1.17.0" />
|
<PackageReference Include="Blazor.ContextMenu" Version="1.17.0" />
|
||||||
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
||||||
<PackageReference Include="FluentFTP" Version="49.0.2" />
|
|
||||||
<PackageReference Include="JWT" Version="10.1.1" />
|
<PackageReference Include="JWT" Version="10.1.1" />
|
||||||
<PackageReference Include="MimeTypes" Version="2.4.1">
|
<PackageReference Include="MimeTypes" Version="2.4.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MoonCore" Version="1.1.6" />
|
<PackageReference Include="MoonCore" Version="1.1.9" />
|
||||||
<PackageReference Include="MoonCoreUI" Version="1.1.4" />
|
<PackageReference Include="MoonCoreUI" Version="1.1.4" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||||
|
@ -110,6 +109,11 @@
|
||||||
<_ContentIncludedByDefault Remove="Features\ScheduleDesigner\UI\Components\ScheduleLinkItem.razor" />
|
<_ContentIncludedByDefault Remove="Features\ScheduleDesigner\UI\Components\ScheduleLinkItem.razor" />
|
||||||
<_ContentIncludedByDefault Remove="Features\ScheduleDesigner\UI\Components\ScheduleNodeItem.razor" />
|
<_ContentIncludedByDefault Remove="Features\ScheduleDesigner\UI\Components\ScheduleNodeItem.razor" />
|
||||||
<_ContentIncludedByDefault Remove="wwwroot\svg\logo.svg" />
|
<_ContentIncludedByDefault Remove="wwwroot\svg\logo.svg" />
|
||||||
|
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\Editor.razor" />
|
||||||
|
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\FileEditor.razor" />
|
||||||
|
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\FileManager.razor" />
|
||||||
|
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\FileUploader.razor" />
|
||||||
|
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\FileView.razor" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Loading…
Reference in a new issue