Refactored css classes to match flyonui. Switched to postgres arrays for permissions. Migrated file manager. Adjusted everything to work with the latest mooncore version

This commit is contained in:
2025-07-12 23:53:43 +02:00
parent eaece9e334
commit d88376f2fb
72 changed files with 2870 additions and 2227 deletions

View File

@@ -1,134 +0,0 @@
using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Fm;
using MoonCore.Blazor.Tailwind.Fm.Models;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Requests.Admin.Sys.Files;
using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.Client.Implementations;
public class SysFileSystemProvider : IFileSystemProvider, ICompressFileSystemProvider
{
private readonly DownloadService DownloadService;
private readonly HttpApiClient HttpApiClient;
private readonly LocalStorageService LocalStorageService;
private readonly string BaseApiUrl = "api/admin/system/files";
public CompressType[] CompressTypes { get; } =
[
new()
{
Extension = "zip",
DisplayName = "ZIP Archive"
},
new()
{
Extension = "tar.gz",
DisplayName = "GZ Compressed Tar Archive"
}
];
public SysFileSystemProvider(
HttpApiClient httpApiClient,
DownloadService downloadService,
LocalStorageService localStorageService
)
{
HttpApiClient = httpApiClient;
DownloadService = downloadService;
LocalStorageService = localStorageService;
}
public async Task<FileSystemEntry[]> List(string path)
{
var entries = await HttpApiClient.GetJson<FileSystemEntryResponse[]>(
$"{BaseApiUrl}/list?path={path}"
);
return entries.Select(x => new FileSystemEntry()
{
Name = x.Name,
Size = x.Size,
CreatedAt = x.CreatedAt,
IsFile = x.IsFile,
UpdatedAt = x.UpdatedAt
}).ToArray();
}
public async Task Create(string path, Stream stream)
{
await Upload(_ => Task.CompletedTask, path, stream);
}
public async Task Move(string oldPath, string newPath)
=> await HttpApiClient.Post($"{BaseApiUrl}/move?oldPath={oldPath}&newPath={newPath}");
public async Task Delete(string path)
=> await HttpApiClient.Delete($"{BaseApiUrl}/delete?path={path}");
public async Task CreateDirectory(string path)
=> await HttpApiClient.Post($"{BaseApiUrl}/mkdir?path={path}");
public async Task<Stream> Read(string path)
=> await HttpApiClient.GetStream($"{BaseApiUrl}/download?path={path}");
public async Task Download(Func<int, Task> updateProgress, string path, string fileName)
{
var accessToken = await LocalStorageService.GetString("AccessToken");
await DownloadService.DownloadUrl(fileName, $"{BaseApiUrl}/download?path={path}",
async (loaded, total) =>
{
var percent = total == 0 ? 0 : (int)Math.Round((float)loaded / total * 100);
await updateProgress.Invoke(percent);
},
onConfigureHeaders: headers => { headers.Add("Authorization", $"Bearer {accessToken}"); }
);
}
public async Task Upload(Func<int, Task> updateProgress, string path, Stream stream)
{
var size = stream.Length;
var chunkSize = ByteConverter.FromMegaBytes(20).Bytes;
var chunks = size / chunkSize;
chunks += size % chunkSize > 0 ? 1 : 0;
for (var chunkId = 0; chunkId < chunks; chunkId++)
{
var percent = (int)Math.Round((chunkId + 1f) / chunks * 100);
await updateProgress.Invoke(percent);
var buffer = new byte[chunkSize];
var bytesRead = await stream.ReadAsync(buffer);
var uploadForm = new MultipartFormDataContent();
uploadForm.Add(new ByteArrayContent(buffer, 0, bytesRead), "file", path);
await HttpApiClient.Post(
$"{BaseApiUrl}/upload?path={path}&totalSize={size}&chunkId={chunkId}",
uploadForm
);
}
}
public async Task Compress(CompressType type, string path, string[] itemsToCompress)
{
await HttpApiClient.Post($"{BaseApiUrl}/compress", new CompressRequest()
{
Type = type.Extension,
Path = path,
ItemsToCompress = itemsToCompress
});
}
public async Task Decompress(CompressType type, string path, string destination)
{
await HttpApiClient.Post($"{BaseApiUrl}/decompress", new DecompressRequest()
{
Type = type.Extension,
Path = path,
Destination = destination
});
}
}

View File

@@ -0,0 +1,88 @@
using MoonCore.Blazor.FlyonUi.Files;
using MoonCore.Blazor.FlyonUi.Files.Manager;
using MoonCore.Helpers;
using Moonlight.Shared.Http.Responses.Admin.Sys;
namespace Moonlight.Client.Implementations;
public class SystemFsAccess : IFsAccess
{
private readonly HttpApiClient ApiClient;
private const string BaseApiUrl = "api/admin/system/files";
public SystemFsAccess(HttpApiClient apiClient)
{
ApiClient = apiClient;
}
public async Task CreateFile(string path)
{
await ApiClient.Post(
$"{BaseApiUrl}/touch?path={path}"
);
}
public async Task CreateDirectory(string path)
{
await ApiClient.Post(
$"{BaseApiUrl}/mkdir?path={path}"
);
}
public async Task<FsEntry[]> List(string path)
{
var entries = await ApiClient.GetJson<FileSystemEntryResponse[]>(
$"{BaseApiUrl}/list?path={path}"
);
return entries.Select(x => new FsEntry()
{
Name = x.Name,
CreatedAt = x.CreatedAt,
IsFolder = x.IsFolder,
Size = x.Size,
UpdatedAt = x.UpdatedAt
}).ToArray();
}
public async Task Move(string oldPath, string newPath)
{
await ApiClient.Post(
$"{BaseApiUrl}/move?oldPath={oldPath}&newPath={newPath}"
);
}
public Task Read(string path, Func<Stream, Task> onHandleData)
{
throw new NotImplementedException();
}
public Task Write(string path, Stream dataStream)
{
throw new NotImplementedException();
}
public async Task Delete(string path)
{
await ApiClient.Delete(
$"{BaseApiUrl}/delete?path={path}"
);
}
public async Task UploadChunk(string path, int chunkId, long chunkSize, long totalSize, byte[] data)
{
using var formContent = new MultipartFormDataContent();
formContent.Add(new ByteArrayContent(data), "file", "file");
await ApiClient.Post(
$"{BaseApiUrl}/upload?path={path}&chunkId={chunkId}&chunkSize={chunkSize}&totalSize={totalSize}",
formContent
);
}
public Task<byte[]> DownloadChunk(string path, int chunkId, long chunkSize)
{
throw new NotImplementedException();
}
}

View File

@@ -12,7 +12,7 @@
<PropertyGroup>
<PackageTags>frontend</PackageTags>
<PackageId>Moonlight.Client</PackageId>
<Version>2.1.1</Version>
<Version>2.1.2</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the client for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
@@ -22,9 +22,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazor-ApexCharts" Version="6.0.0" />
<PackageReference Include="MoonCore" Version="1.9.1" />
<PackageReference Include="MoonCore" Version="1.9.2" />
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.4" />
<PackageReference Include="MoonCore.Blazor.FlyonUi" Version="1.0.7" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
@@ -58,6 +58,11 @@
<Folder Include="Styles\" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\css\style.min.css" />
<_ContentIncludedByDefault Remove="Properties\launchSettings.json" />
</ItemGroup>
<ItemGroup>
<None Include="Styles/mappings/*" Pack="true" PackagePath="Styles/mappings/" />
<None Include="Moonlight.Client.targets" Pack="true" PackagePath="build\Moonlight.Client.targets" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
<Project>
<ItemGroup>
<StylesFilesToCopy Include="$(MSBuildThisFileDirectory)../Styles/**/*.*"/>
</ItemGroup>
<Target Name="CopyContent" BeforeTargets="Build">
<Copy SourceFiles="@(StylesFilesToCopy)" DestinationFolder="$(ProjectDir)Styles/%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@@ -1,14 +0,0 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5165",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -104,7 +104,7 @@ public class RemoteAuthStateManager : AuthenticationStateManager
[
new Claim("username", checkData.Username),
new Claim("email", checkData.Email),
new Claim("permissions", checkData.Permissions)
new Claim("permissions", string.Join(";", checkData.Permissions))
],
"RemoteAuthStateManager"
)

View File

@@ -0,0 +1,513 @@
!bg-base-100
!border-base-content/40
!border-none
!flex
!font-medium
!font-semibold
!h-2.5
!justify-between
!me-1.5
!ms-auto
!px-2.5
!rounded-full
!text-sm
!w-2.5
*:[grid-area:1/1]
*:first:rounded-tl-lg
*:last:rounded-tr-lg
-left-4
-ml-4
-translate-x-full
-translate-y-1/2
absolute
accordion
accordion-bordered
accordion-toggle
active
active-tab:bg-primary
active-tab:text-base-content
advance-select-menu
advance-select-option
advance-select-tag
advance-select-toggle
alert
alert-error
alert-outline
alert-soft
align-bottom
align-middle
animate-bounce
animate-ping
aria-[current='page']:text-bg-soft-primary
avatar
avatar-away-bottom
avatar-away-top
avatar-busy-bottom
avatar-busy-top
avatar-offline-bottom
avatar-offline-top
avatar-online-bottom
avatar-online-top
avatar-placeholder
badge
badge-error
badge-info
badge-outline
badge-primary
badge-soft
badge-success
bg-background
bg-background/60
bg-base-100
bg-base-150
bg-base-200
bg-base-200/50
bg-base-300
bg-base-300/45
bg-base-300/50
bg-base-300/60
bg-error
bg-info
bg-primary
bg-primary/5
bg-success
bg-transparent
bg-warning
block
blur
border
border-0
border-2
border-b
border-base-content
border-base-content/20
border-base-content/40
border-base-content/5
border-dashed
border-t
border-transparent
bottom-0
bottom-full
break-words
btn
btn-accent
btn-active
btn-circle
btn-disabled
btn-error
btn-info
btn-outline
btn-primary
btn-secondary
btn-sm
btn-soft
btn-square
btn-success
btn-text
btn-warning
card
card-alert
card-body
card-border
card-footer
card-header
card-title
carousel
carousel-body
carousel-next
carousel-prev
carousel-slide
chat
chat-avatar
chat-bubble
chat-footer
chat-header
chat-receiver
chat-sender
checkbox
checkbox-primary
checkbox-xs
col-span-1
collapse
combo-box-selected:block
combo-box-selected:dropdown-active
complete
container
contents
cursor-default
cursor-not-allowed
cursor-pointer
diff
disabled
divide-base-150/60
divide-y
drop-shadow
dropdown
dropdown-disabled
dropdown-item
dropdown-menu
dropdown-open:opacity-100
dropdown-open:rotate-180
dropdown-toggle
duration-300
duration-500
ease-in-out
ease-linear
end-3
file-upload-complete:progress-success
fill-black
filter
filter-reset
fixed
flex
flex-1
flex-col
flex-grow
flex-nowrap
flex-row
flex-shrink-0
flex-wrap
focus-visible:outline-none
focus-within:border-primary
focus:border-primary
focus:outline-1
focus:outline-none
focus:outline-primary
focus:ring-0
font-bold
font-inter
font-medium
font-normal
font-semibold
gap-0.5
gap-1
gap-1.5
gap-2
gap-3
gap-4
gap-5
gap-6
gap-x-1
gap-x-2
gap-x-3
gap-y-1
gap-y-3
grid
grid-cols-1
grid-flow-col
grow
grow-0
h-12
h-2
h-32
h-64
h-8
h-auto
h-full
h-screen
helper-text
hidden
hover:bg-primary/5
hover:bg-transparent
hover:text-base-content
hover:text-base-content/60
hover:text-primary
image-full
inline
inline-block
inline-flex
inline-grid
input
input-floating
input-floating-label
input-lg
input-md
input-sm
input-xl
inset-0
inset-y-0
inset-y-2
invisible
is-invalid
is-valid
isolate
italic
items-center
items-end
items-start
join
join-item
justify-between
justify-center
justify-end
justify-start
justify-stretch
label-text
leading-3
leading-3.5
leading-6
left-0
lg:bg-base-100/20
lg:flex
lg:gap-y-0
lg:grid-cols-2
lg:hidden
lg:justify-end
lg:justify-start
lg:min-w-0
lg:p-10
lg:pb-5
lg:pl-64
lg:pr-3.5
lg:pt-5
lg:ring-1
lg:ring-base-content/10
lg:rounded-lg
lg:shadow-xs
list-disc
list-inside
loading
loading-lg
loading-sm
loading-spinner
loading-xl
loading-xs
lowercase
m-10
mask
max-lg:flex-col
max-lg:hidden
max-w-7xl
max-w-80
max-w-full
max-w-lg
max-w-sm
max-w-xl
mb-0.5
mb-1
mb-2
mb-3
mb-4
mb-5
md:table-cell
md:text-3xl
me-1
me-1.5
me-2
me-5
menu
menu-active
menu-disabled
menu-dropdown
menu-dropdown-show
menu-focus
menu-horizontal
menu-title
min-h-0
min-h-svh
min-w-0
min-w-28
min-w-48
min-w-60
min-w-[100px]
ml-3
ml-4
modal
modal-content
modal-dialog
modal-middle
modal-title
mr-4
ms-1
ms-2
ms-3
mt-1
mt-1.5
mt-10
mt-12
mt-2
mt-2.5
mt-3
mt-4
mt-5
mt-8
mx-1
mx-auto
my-3
my-auto
opacity-0
opacity-100
open
origin-top-left
outline
outline-0
overflow-hidden
overflow-x-auto
overflow-y-auto
overlay-open:duration-50
overlay-open:opacity-100
p-0.5
p-1
p-2
p-3
p-4
p-5
p-6
p-8
pin-input
pin-input-underline
placeholder-base-content/60
pointer-events-auto
pointer-events-none
progress
progress-bar
progress-indeterminate
progress-primary
pt-0.5
pt-3
px-1.5
px-2
px-3
px-4
px-5
py-0.5
py-1.5
py-2
py-2.5
py-6
radio
range
relative
resize
ring-0
ring-1
ring-white/10
rounded-box
rounded-field
rounded-full
rounded-lg
rounded-md
rounded-t-lg
row-active
row-hover
rtl:!mr-0
select
select-disabled:opacity-40
select-disabled:pointer-events-none
select-floating
select-floating-label
selected
selected:select-active
shadow-base-300/20
shadow-lg
shadow-xs
shrink-0
size-10
size-4
size-5
size-8
skeleton
skeleton-animated
sm:auto-cols-max
sm:flex
sm:items-center
sm:items-end
sm:justify-between
sm:justify-end
sm:max-w-2xl
sm:max-w-3xl
sm:max-w-4xl
sm:max-w-5xl
sm:max-w-6xl
sm:max-w-7xl
sm:max-w-lg
sm:max-w-md
sm:max-w-xl
sm:mb-0
sm:mt-5
sm:mt-6
sm:p-6
sm:py-2
sm:text-sm/5
space-x-1
space-y-1
space-y-4
sr-only
static
status
status-error
sticky
switch
tab
tab-active
table
table-pin-cols
table-pin-rows
tabs
tabs-bordered
tabs-lg
tabs-lifted
tabs-md
tabs-sm
tabs-xl
tabs-xs
text-2xl
text-4xl
text-accent
text-base
text-base-content
text-base-content/40
text-base-content/50
text-base-content/60
text-base-content/70
text-base-content/80
text-base/6
text-center
text-error
text-error-content
text-gray-400
text-info
text-info-content
text-left
text-lg
text-primary
text-primary-content
text-sm
text-sm/5
text-success
text-success-content
text-warning
text-warning-content
text-xl
text-xs
text-xs/5
textarea
textarea-floating
textarea-floating-label
theme-controller
tooltip
tooltip-content
top-0
top-1/2
top-full
transform
transition
transition-all
transition-opacity
translate-x-0
truncate
underline
uppercase
validate
w-0
w-0.5
w-12
w-4
w-56
w-64
w-fit
w-full
whitespace-nowrap
z-10
z-40
z-50

View File

@@ -1,11 +1,11 @@
<div class="card card-body">
<div class="flex justify-between">
<p class="text-xl font-semibold text-slate-200">
<p class="text-xl font-semibold text-base-content">
@Text
</p>
<i class="@Icon text-4xl text-primary"></i>
</div>
<p class="text-base text-slate-300">@Title</p>
<p class="text-base-content/80">@Title</p>
</div>
@code

View File

@@ -17,7 +17,7 @@
<i class="icon-palette mix-blend-exclusion"></i>
<span class="ms-2 mix-blend-exclusion">@(currentHex)</span>
</label>
<button @onclick="Reset" class="btn btn-danger">
<button @onclick="Reset" class="btn btn-error">
<i class="icon-rotate-ccw"></i>
</button>
</div>

View File

@@ -5,7 +5,7 @@
<div class="animate-shimmer bg-gradient-to-r from-violet-400 via-sky-400 to-purple-400 bg-clip-text font-semibold text-transparent text-3xl" style="animation-duration: 5s; background-size: 200% 100%">
Welcome, @(Username)
</div>
<div class="text-gray-200 text-2xl">What do you want to do today?</div>
<div class="text-base-content/90 text-2xl">What do you want to do today?</div>
</div>
</div>

View File

@@ -1,13 +1,14 @@
@using Moonlight.Client.UI.Partials
@using MoonCore.Blazor.FlyonUi.Files.Drop
@inherits LayoutComponentBase
<div class="relative isolate flex min-h-svh w-full h-screen max-lg:flex-col bg-gray-900 lg:bg-gray-950/80">
<div class="relative isolate flex min-h-svh w-full h-screen max-lg:flex-col bg-background">
<AppSidebar Layout="this"/>
<AppHeader Layout="this" />
<main class="h-full flex flex-1 flex-col lg:pb-5 lg:min-w-0 lg:pt-5 lg:pr-3.5 lg:pl-64">
<div class="h-full grow p-6 lg:rounded-lg lg:p-10 lg:ring-1 lg:shadow-xs lg:bg-gray-900/80 lg:ring-white/10">
<div class="grow p-6 lg:rounded-lg lg:p-10 lg:ring-1 lg:shadow-xs lg:bg-base-100/20 lg:ring-base-content/10">
<div class="h-full mx-auto max-w-7xl">
<CascadingValue Value="this" IsFixed="true">
@Body
@@ -15,20 +16,21 @@
</div>
</div>
</main>
</div>
<ToastLauncher/>
<ModalLauncher/>
<DropHandler />
<div id="blazor-error-ui" class="fixed bottom-0 left-0 w-full z-50">
<div class="bg-error text-white p-4 flex flex-row justify-between items-center">
<div class="bg-error text-base-content p-4 flex flex-row justify-between items-center">
<div class="flex items-center">
<i class="icon-bomb text-lg text-white me-2"></i>
<i class="icon-bomb text-lg text-base-content me-2"></i>
<span>An unhandled error has occurred.</span>
</div>
<div>
<a href="#" class="reload text-white underline mr-4">Reload</a>
<a href="#" class="reload text-base-content underline mr-4">Reload</a>
<a href="#" class="dismiss hidden">🗙</a>
</div>
</div>

View File

@@ -2,11 +2,11 @@
@inject NavigationManager Navigation
<header class="flex items-center px-4 lg:hidden border-b border-white/5">
<header class="flex items-center px-4 lg:hidden border-b border-base-content/5">
<div class="py-2.5">
<span class="relative">
<button @onclick="Layout.ToggleMobileNavigation" aria-label="Open navigation"
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 sm:text-sm/5 text-white"
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 sm:text-sm/5 text-base-content"
type="button">
<i class="icon-menu text-xl"></i>
</button>
@@ -18,12 +18,12 @@
</div>
<div class="flex items-center gap-3">
<span class="relative">
<div class="relative flex min-w-0 cursor-default items-center gap-3 rounded-lg p-2 text-left text-base/6 font-medium sm:text-sm/5 text-white">
<div class="relative flex min-w-0 cursor-default items-center gap-3 rounded-lg p-2 text-left text-base/6 font-medium sm:text-sm/5 text-base-content">
<div data-slot="avatar"
class="inline-grid shrink-0 align-middle">
<img
class="h-8 rounded-full"
src="/img/pfp_placeholder.png"
src="/svg/logo.svg"
alt=""/>
</div>
</div>

View File

@@ -17,17 +17,17 @@
<div class="fixed inset-y-0 left-0 w-64 max-lg:hidden">
<nav class="flex h-full min-h-0 flex-col">
<div class="flex flex-col border-b p-4 border-white/5">
<div class="flex flex-col border-b p-4 border-base-content/5">
<span class="relative">
<div type="button"
class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-lg font-medium text-gray-100">
class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-lg font-medium text-base-content">
<span
class="inline-grid shrink-0 align-middle">
<img class="h-8 rounded-full"
src="/svg/logo.svg"
alt=""/>
</span>
<span class="truncate">Moonlight</span>
<span class="truncate">Moonlight v2.1</span>
</div>
</span>
</div>
@@ -37,7 +37,7 @@
{
if (!string.IsNullOrEmpty(item.Key))
{
<h3 class="mt-4 px-2 text-sm/5 font-medium text-gray-400">
<h3 class="mt-4 px-2 text-sm/5 font-medium text-base-content/40">
@item.Key
</h3>
}
@@ -51,10 +51,10 @@
@if (isMatch)
{
<div class="relative">
<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-white"
<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-primary"
style="opacity: 1;">
</span>
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal bg-white/5 sm:py-2 sm:text-sm/5 text-white"
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal bg-primary/5 sm:py-2 sm:text-sm/5 text-base-content"
href="@sidebarItem.Path">
<i class="@sidebarItem.Icon text-lg"></i>
<span class="truncate">
@@ -66,7 +66,7 @@
else
{
<div class="relative">
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal sm:py-2 sm:text-sm/5 text-white hover:bg-white/5"
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal sm:py-2 sm:text-sm/5 text-base-content hover:bg-primary/5"
href="@sidebarItem.Path">
<i class="@sidebarItem.Icon text-lg"></i>
<span class="truncate">
@@ -79,9 +79,9 @@
}
</div>
</div>
<div class="flex flex-col border-t p-4 max-lg:hidden border-white/5 mt-2.5">
<div class="flex flex-col border-t p-4 max-lg:hidden border-base-content/5 mt-2.5">
<div
class="flex w-full items-center px-2 py-2.5 gap-6 rounded-lg text-left text-base/6 font-medium sm:py-2 sm:text-sm/5 text-white">
class="flex w-full items-center px-2 py-2.5 gap-6 rounded-lg text-left text-base/6 font-medium sm:py-2 sm:text-sm/5 text-base-content">
<div class="flex min-w-0 items-center gap-3">
<span class="inline-grid shrink-0 align-middle">
<img class="h-8 rounded-full"
@@ -89,10 +89,10 @@
alt=""/>
</span>
<div class="min-w-0">
<div class="block truncate text-sm/5 font-medium text-white">
<div class="block truncate text-sm/5 font-medium text-base-content">
@Username
</div>
<div class="block truncate text-xs/5 font-normal text-gray-400">
<div class="block truncate text-xs/5 font-normal text-base-content/40">
@Email
</div>
</div>
@@ -108,22 +108,23 @@
<div
class="lg:hidden z-50 transition-opacity ease-linear duration-300 @(Layout.ShowMobileNavigation ? "opacity-100" : "opacity-0 pointer-events-none")"
role="dialog" tabindex="-1">
<div class="fixed inset-0 bg-black/30"></div>
<div class="fixed inset-0 bg-background/60"></div>
<div class="fixed inset-y-0 w-full max-w-80 p-2">
<div
class="relative flex h-full flex-col rounded-lg shadow-xs ring-1 bg-gray-900 ring-white/10 transition ease-in-out duration-300 transform @(Layout.ShowMobileNavigation ? "translate-x-0" : "-translate-x-full")">
<div class="border-b p-4 border-white/5 flex justify-between px-5 pt-3">
<div class="flex items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 font-medium sm:py-2 sm:text-sm/5 text-white">
class="relative flex h-full flex-col rounded-lg shadow-xs ring-1 bg-base-300 ring-white/10 transition ease-in-out duration-300 transform @(Layout.ShowMobileNavigation ? "translate-x-0" : "-translate-x-full")">
<div class="border-b p-4 border-base-content/5 flex justify-between px-5 pt-3">
<div
class="flex items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 font-medium sm:py-2 sm:text-sm/5 text-base-content">
<div data-slot="avatar"
class="inline-grid shrink-0 align-middle">
<img
class="h-8 rounded-full" src="/svg/logo.svg" alt=""/>
class="h-8 rounded-full" src="/placeholder.jpg" alt=""/>
</div>
<div class="truncate">Moonlight</div>
<div class="truncate">Moonlight v2.1</div>
</div>
<button @onclick="Layout.ToggleMobileNavigation" aria-label="Close navigation" type="button"
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 text-white">
class="relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 text-base-content">
<i class="icon-x text-lg"></i>
</button>
</div>
@@ -149,10 +150,10 @@
@if (isMatch)
{
<div class="relative">
<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-white"
<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-primary"
style="opacity: 1;">
</span>
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal bg-white/5 sm:py-2 sm:text-sm/5 text-white"
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal bg-primary/5 sm:py-2 sm:text-sm/5 text-base-content"
href="@sidebarItem.Path">
<i class="@sidebarItem.Icon text-lg"></i>
<span class="truncate">
@@ -164,7 +165,7 @@
else
{
<div class="relative">
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal sm:py-2 sm:text-sm/5 text-white hover:bg-white/5"
<a class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-base/6 font-normal sm:py-2 sm:text-sm/5 text-base-content hover:bg-primary/5"
href="@sidebarItem.Path">
<i class="@sidebarItem.Icon text-lg"></i>
<span class="truncate">
@@ -180,8 +181,7 @@
<div class="mt-8 flex-1"></div>
<div class="flex flex-col gap-0.5">
<div class="relative">
<a class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 sm:py-2 sm:text-sm/5 text-white"
<a class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 sm:py-2 sm:text-sm/5 text-base-content"
href="#" @onclick:preventDefault @onclick="Logout">
<i class="icon-log-out"></i>
<span class="truncate">Logout</span>

View File

@@ -4,22 +4,22 @@
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using MoonCore.Blazor.Tailwind.Input2
@using MoonCore.Blazor.FlyonUi.Forms
@using MoonCore.Blazor.FlyonUi.Helpers
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@inject ToastService ToastService
@inject AlertService AlertService
@* @inject DownloadService DownloadService *@
@inject DownloadService DownloadService
<PageHeader Title="Create API Key">
<a href="/admin/api" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Create
</WButton>
</PageHeader>
@@ -28,21 +28,21 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Description</label>
<label class="block text-sm font-medium leading-6 text-base-content">Description</label>
<div class="mt-2">
<input @bind="Request.Description" type="text" autocomplete="off" class="form-input w-full">
<input @bind="Request.Description" type="text" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Permissions</label>
<label class="block text-sm font-medium leading-6 text-base-content">Permissions</label>
<div class="mt-2">
<InputTags @bind-Value="Permissions" />
<InputTags Value="Permissions" />
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Expires at</label>
<label class="block text-sm font-medium leading-6 text-base-content">Expires at</label>
<div class="mt-2">
<input @bind="Request.ExpiresAt" type="date" autocomplete="off" class="form-input w-full">
<input @bind="Request.ExpiresAt" type="date" autocomplete="off" class="input w-full">
</div>
</div>
</div>
@@ -54,7 +54,7 @@
private HandleForm Form;
private CreateApiKeyRequest Request;
private string[] Permissions = [];
private List<string> Permissions = [];
protected override void OnInitialized()
{
@@ -63,12 +63,12 @@
private async Task OnSubmit()
{
Request.PermissionsJson = JsonSerializer.Serialize(Permissions);
Request.Permissions = Permissions.ToArray();
Request.ExpiresAt = Request.ExpiresAt.ToUniversalTime();
var response = await ApiClient.PostJson<CreateApiKeyResponse>("api/admin/apikeys", Request);
await DownloadService.DownloadString(
await DownloadService.Download(
$"moonlight-key-{response.Id}.txt",
response.Secret
);

View File

@@ -23,7 +23,7 @@
</div>
<div class="col-span-1 card card-body border-l-4 border-tertiary">
<p class="font-medium tracking-wide text-slate-100">
<p class="font-medium tracking-wide text-base-content">
Learn about the api usage
</p>
<a href="https://help.moonlightpanel.xyz" target="_blank" class="mt-2 flex items-center justify-between text-primary">
@@ -35,7 +35,7 @@
</div>
<div class="col-span-1 card card-body border-l-4 border-success">
<p class="font-medium tracking-wide text-slate-100">
<p class="font-medium tracking-wide text-base-content">
Open API Specification
</p>
<a href="/api/swagger/main" target="_blank" class="mt-2 flex items-center justify-between text-primary">
@@ -61,7 +61,7 @@
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.Description)" Name="Description"/>
<DataTableColumn TItem="ApiKeyResponse" Field="@(x => x.ExpiresAt)" Name="Expires at">
<ColumnTemplate>
@(Formatter.FormatDate(context.ExpiresAt))
@(Formatter.FormatDate(context.ExpiresAt.UtcDateTime))
</ColumnTemplate>
</DataTableColumn>
<DataTableColumn TItem="ApiKeyResponse">
@@ -72,7 +72,7 @@
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
class="text-danger">
class="text-error">
<i class="icon-trash text-base"></i>
</a>
</div>

View File

@@ -12,11 +12,11 @@
<LazyLoader Load="Load">
<PageHeader Title="Update API Key">
<a href="/admin/api" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Update
</WButton>
</PageHeader>
@@ -25,9 +25,9 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Description</label>
<label class="block text-sm font-medium leading-6 text-base-content">Description</label>
<div class="mt-2">
<input @bind="Request.Description" type="text" autocomplete="off" class="form-input w-full">
<input @bind="Request.Description" type="text" autocomplete="off" class="input w-full">
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
@page "/admin/system/advanced"
@using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.FlyonUi.Helpers
@using MoonCore.Helpers
@attribute [Authorize(Policy = "permissions:admin.system.advanced")]
@@ -36,6 +37,6 @@
{
var stream = await ApiClient.GetStream("api/admin/system/advanced/frontend");
await DownloadService.DownloadStream("frontend.zip", stream);
await DownloadService.Download("frontend.zip", stream);
}
}

View File

@@ -1,6 +1,7 @@
@page "/admin/system/diagnose"
@using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.FlyonUi.Helpers
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Sys
@using Moonlight.Shared.Http.Responses.Admin.Sys
@@ -47,7 +48,7 @@
<div class="@(DropdownOpen ? "" : "hidden")">
<LazyLoader Load="Load">
<div class="mb-2 py-2 border-b border-gray-700 flex items-center gap-3">
<div class="mb-2 py-2 border-b border-base-content/70 flex items-center gap-3">
<input id="selectall_checkbox" @bind="SelectAll" type="checkbox" class="form-checkbox">
<label for="selectall_checkbox">Select all</label>
</div>
@@ -99,7 +100,7 @@
if (!SelectAll)
{
// filter the providers which have been selected if not all providers have been selected
// Filter the providers which have been selected if not all providers have been selected
request.Providers = AvailableProviders
.Where(x => x.Value)
.Select(x => x.Key.Type)
@@ -108,7 +109,7 @@
var stream = await ApiClient.PostStream("api/admin/system/diagnose", request);
await DownloadService.DownloadStream("diagnose.zip", stream);
await DownloadService.Download("diagnose.zip", stream);
}

View File

@@ -3,31 +3,28 @@
@using Microsoft.AspNetCore.Authorization
@using MoonCore.Blazor.Services
@using MoonCore.Helpers
@using MoonCore.Blazor.Tailwind.Fm
@using Moonlight.Client.Implementations
@using MoonCore.Blazor.FlyonUi.Files.Manager
@attribute [Authorize(Policy = "permissions:admin.system.overview")]
@inject HttpApiClient ApiClient
@inject DownloadService DownloadService
@inject LocalStorageService LocalStorageService
<div class="mb-5">
<NavTabs Index="2" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks"/>
</div>
<FileManager FileSystemProvider="FileSystemProvider" MaxUploadSize="4096"/>
<FileManager FsAccess="FsAccess" TransferChunkSize="TransferChunkSize" UploadLimit="UploadLimit"/>
@code
{
private IFileSystemProvider FileSystemProvider;
private IFsAccess FsAccess;
private static readonly long TransferChunkSize = ByteConverter.FromMegaBytes(20).Bytes;
private static readonly long UploadLimit = ByteConverter.FromGigaBytes(20).Bytes;
protected override void OnInitialized()
{
FileSystemProvider = new SysFileSystemProvider(
ApiClient,
DownloadService,
LocalStorageService
);
FsAccess = new SystemFsAccess(ApiClient);
}
}

View File

@@ -22,8 +22,8 @@
<div class="card card-body">
<div class="flex justify-center">
<WButton OnClick="Restart" CssClasses="btn btn-danger w-full">
<i class="icon-repeat text-xl text-white me-2"></i>
<WButton OnClick="Restart" CssClasses="btn btn-error w-full">
<i class="icon-repeat text-xl text-base-content me-2"></i>
Restart/Shutdown
</WButton>
</div>

View File

@@ -3,6 +3,7 @@
@using System.Text.Json
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using MoonCore.Blazor.FlyonUi.Forms
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@@ -10,11 +11,11 @@
<PageHeader Title="Create User">
<a href="/admin/users" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Create
</WButton>
</PageHeader>
@@ -23,27 +24,27 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Username</label>
<label class="block text-sm font-medium leading-6 text-base-content">Username</label>
<div class="mt-2">
<input @bind="Request.Username" type="text" autocomplete="off" class="form-input w-full">
<input @bind="Request.Username" type="text" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Email</label>
<label class="block text-sm font-medium leading-6 text-base-content">Email</label>
<div class="mt-2">
<input @bind="Request.Email" type="email" autocomplete="off" class="form-input w-full">
<input @bind="Request.Email" type="email" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Permissions</label>
<label class="block text-sm font-medium leading-6 text-base-content">Permissions</label>
<div class="mt-2">
<InputTags @bind-Value="Permissions" />
<InputTags Value="Permissions" />
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Password</label>
<label class="block text-sm font-medium leading-6 text-base-content">Password</label>
<div class="mt-2">
<input @bind="Request.Password" type="password" autocomplete="off" class="form-input w-full">
<input @bind="Request.Password" type="password" autocomplete="off" class="input w-full">
</div>
</div>
</div>
@@ -55,7 +56,7 @@
private HandleForm Form;
private CreateUserRequest Request;
private string[] Permissions = [];
private List<string> Permissions = [];
protected override void OnInitialized()
{
@@ -64,7 +65,7 @@
private async Task OnSubmit()
{
Request.PermissionsJson = JsonSerializer.Serialize(Permissions);
Request.Permissions = Permissions.ToArray();
await ApiClient.Post("api/admin/users", Request);

View File

@@ -32,8 +32,8 @@
</a>
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
class="text-danger">
<i class="icon-trash text-base"></i>
class="text-error">
<i class="icon-trash text-base"></i>
</a>
</div>
</ColumnTemplate>

View File

@@ -4,6 +4,7 @@
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonCore.Blazor.FlyonUi.Forms
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@@ -12,11 +13,11 @@
<LazyLoader Load="Load">
<PageHeader Title="Update User">
<a href="/admin/users" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Update
</WButton>
</PageHeader>
@@ -25,27 +26,27 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Username</label>
<label class="block text-sm font-medium leading-6 text-base-content">Username</label>
<div class="mt-2">
<input @bind="Request.Username" type="text" autocomplete="off" class="form-input w-full">
<input @bind="Request.Username" type="text" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Email</label>
<label class="block text-sm font-medium leading-6 text-base-content">Email</label>
<div class="mt-2">
<input @bind="Request.Email" type="email" autocomplete="off" class="form-input w-full">
<input @bind="Request.Email" type="email" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Permissions</label>
<label class="block text-sm font-medium leading-6 text-base-content">Permissions</label>
<div class="mt-2">
<InputTags @bind-Value="Permissions" />
<InputTags Value="Permissions" />
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-white">Password</label>
<label class="block text-sm font-medium leading-6 text-base-content">Password</label>
<div class="mt-2">
<input @bind="Request.Password" type="password" autocomplete="off" class="form-input w-full">
<input @bind="Request.Password" type="password" autocomplete="off" class="input w-full">
</div>
</div>
</div>
@@ -60,25 +61,25 @@
private HandleForm Form;
private UpdateUserRequest Request;
private string[] Permissions = [];
private List<string> Permissions = [];
private async Task Load(LazyLoader _)
{
var detail = await ApiClient.GetJson<UserResponse>($"api/admin/users/{Id}");
Permissions = JsonSerializer.Deserialize<string[]>(detail.PermissionsJson) ?? [];
Permissions = detail.Permissions.ToList();
Request = new()
{
Email = detail.Email,
PermissionsJson = detail.PermissionsJson,
Permissions = detail.Permissions,
Username = detail.Username
};
}
private async Task OnSubmit()
{
Request.PermissionsJson = JsonSerializer.Serialize(Permissions);
Request.Permissions = Permissions.ToArray();
await ApiClient.Patch($"api/admin/users/{Id}", Request);

View File

@@ -1,316 +0,0 @@
window.moonCore = {
window: {
getSize: function () {
return [window.innerWidth, window.innerHeight];
}
},
keyBinds: {
storage: {},
registerHotkey: function (key, modifier, action, dotNetObjRef) {
const hotkeyListener = async (event) => {
if (event.code === key && (!modifier || event[modifier + 'Key'])) {
event.preventDefault();
await dotNetObjRef.invokeMethodAsync("OnHotkeyPressed", action);
}
};
moonCore.keyBinds.storage[`${key}${modifier}`] = hotkeyListener;
window.addEventListener('keydown', hotkeyListener);
},
unregisterHotkey: function (key, modifier) {
const listenerKey = `${key}${modifier}`;
if (moonCore.keyBinds.storage[listenerKey]) {
window.removeEventListener('keydown', moonCore.keyBinds.storage[listenerKey]);
delete moonCore.keyBinds.storage[listenerKey];
}
}
},
downloadService: {
download: async function (fileName, contentStreamReference, id, reportRef) {
const promise = new Promise(async resolve => {
const stream = await contentStreamReference.stream();
const reader = stream.getReader();
let lastReportTime = 0;
let receivedLength = 0; // Track downloaded size
let chunks = []; // Store downloaded chunks
while (true) {
const {done, value} = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
if (reportRef) {
const now = Date.now();
if (now - lastReportTime >= 500) { // Only log once per second
await reportRef.invokeMethodAsync("ReceiveReport", id, receivedLength, -1, false);
lastReportTime = now;
}
}
}
// Combine chunks into a single Blob
const blob = new Blob(chunks);
this.downloadBlob(fileName, blob);
if (reportRef)
await reportRef.invokeMethodAsync("ReceiveReport", id, receivedLength, -1, true);
resolve();
});
await promise;
},
downloadUrl: async function (fileName, url, reportRef, id, headers) {
const promise = new Promise(async resolve => {
let loadRequest = new XMLHttpRequest();
let lastReported = Date.now();
loadRequest.open("GET", url, true);
for(let headerKey in headers) {
loadRequest.setRequestHeader(headerKey, headers[headerKey]);
}
loadRequest.responseType = "blob";
if (reportRef) {
loadRequest.onprogress = async ev => {
const now = Date.now();
if (now - lastReported >= 500) {
await reportRef.invokeMethodAsync("ReceiveReport", id, ev.loaded, ev.total, false);
lastReported = now;
}
};
loadRequest.onloadend = async ev => {
await reportRef.invokeMethodAsync("ReceiveReport", id, ev.loaded, ev.total, true);
}
}
loadRequest.onload = _ => {
this.downloadBlob(fileName, loadRequest.response);
resolve();
}
loadRequest.send();
});
await promise;
},
downloadBlob: function (fileName, blob)
{
const url = URL.createObjectURL(blob);
// Trigger file download
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = fileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
}
},
fileManager: {
uploadCache: [],
addFilesToCache: async function(id) {
let files = document.getElementById(id).files;
for (let i = 0; i < files.length; i++) {
moonCore.fileManager.uploadCache.push(files[i]);
}
await this.ref.invokeMethodAsync("TriggerUpload", moonCore.fileManager.uploadCache.length);
},
getNextFromCache: async function() {
if(moonCore.fileManager.uploadCache.length === 0)
return null;
let nextItem = moonCore.fileManager.uploadCache.pop();
if(!nextItem)
return null;
let file;
let path;
if(nextItem instanceof File)
{
file = nextItem;
path = file.name;
}
else
{
file = await this.openFileEntry(nextItem);
path = nextItem.fullPath;
}
if(file.size === 0)
{
return {
path: null,
stream: null,
left: moonCore.fileManager.uploadCache.length
}
}
let stream = await this.createStreamRef(file);
return {
path: path,
stream: stream,
left: moonCore.fileManager.uploadCache.length
};
},
openFileEntry: async function (fileEntry) {
const promise = new Promise(resolve => {
fileEntry.file(file => {
resolve(file);
}, err => console.log(err));
});
return await promise;
},
createStreamRef: async function (processedFile) {
// Prevent uploads of empty files
if (processedFile.size <= 0) {
console.log("Skipping upload of '" + processedFile.name + "' as its empty");
return null;
}
const fileReader = new FileReader();
const readerPromise = new Promise(resolve => {
fileReader.addEventListener("loadend", ev => {
resolve(fileReader.result)
});
});
fileReader.readAsArrayBuffer(processedFile);
const arrayBuffer = await readerPromise;
return DotNet.createJSStreamReference(arrayBuffer);
},
setup: function (id, callbackRef) {
this.ref = callbackRef;
// Check which features are supported by the browser
const supportsFileSystemAccessAPI =
'getAsFileSystemHandle' in DataTransferItem.prototype;
const supportsWebkitGetAsEntry =
'webkitGetAsEntry' in DataTransferItem.prototype;
// This is the drag and drop zone.
const elem = document.getElementById(id);
// Prevent navigation.
elem.addEventListener('dragover', (e) => {
e.preventDefault();
});
elem.addEventListener('drop', async (e) => {
// Prevent navigation.
e.preventDefault();
if (!supportsFileSystemAccessAPI && !supportsWebkitGetAsEntry) {
// Cannot handle directories.
console.log("Cannot handle directories");
return;
}
this.getAllWebkitFileEntries(e.dataTransfer.items).then(async value => {
value.forEach(a => moonCore.fileManager.uploadCache.push(a));
await this.ref.invokeMethodAsync("TriggerUpload", this.uploadCache.length);
});
});
},
getAllWebkitFileEntries: async function (dataTransferItemList) {
function readAllEntries(reader) {
return new Promise((resolve, reject) => {
const entries = [];
function readEntries() {
reader.readEntries((batch) => {
if (batch.length === 0) {
resolve(entries);
} else {
entries.push(...batch);
readEntries();
}
}, reject);
}
readEntries();
});
}
async function traverseEntry(entry) {
if (entry.isFile) {
return [entry];
} else if (entry.isDirectory) {
const reader = entry.createReader();
const entries = await readAllEntries(reader);
const subEntries = await Promise.all(entries.map(traverseEntry));
return subEntries.flat();
}
return [];
}
const entries = [];
// Convert DataTransferItemList to entries
for (let i = 0; i < dataTransferItemList.length; i++) {
const item = dataTransferItemList[i];
const entry = item.webkitGetAsEntry();
if (entry) {
entries.push(entry);
}
}
// Traverse all entries and collect file entries
const allFileEntries = await Promise.all(entries.map(traverseEntry));
return allFileEntries.flat();
}
},
codeEditor: {
instances: new Map(),
attach: function (id, options) {
const editor = ace.edit(id, options);
moonCore.codeEditor.instances.set(id, editor);
},
updateOptions: function (id, options) {
const editor = moonCore.codeEditor.instances.get(id);
editor.setOptions(options);
},
getValue: function (id) {
const editor = moonCore.codeEditor.instances.get(id);
return editor.getValue();
},
destroy: function (id){
const editor = moonCore.codeEditor.instances.get(id);
if(!editor)
return;
editor.destroy();
editor.container.remove();
moonCore.codeEditor.instances.delete(id);
}
}
}