Merge branch 'main' into CleanupSystem

This commit is contained in:
Daniel Balk 2023-04-03 00:17:52 +02:00 committed by GitHub
commit dd56c7ad87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 2232 additions and 784 deletions

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Helpers.Files;
public class ContextAction
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public Action<FileData> Action { get; set; }
}

View file

@ -0,0 +1,24 @@
namespace Moonlight.App.Helpers.Files;
public abstract class FileAccess : ICloneable
{
public string CurrentPath { get; set; } = "/";
public abstract Task<FileData[]> Ls();
public abstract Task Cd(string dir);
public abstract Task Up();
public abstract Task SetDir(string dir);
public abstract Task<string> Read(FileData fileData);
public abstract Task Write(FileData fileData, string content);
public abstract Task Upload(string name, Stream stream, Action<int>? progressUpdated = null);
public abstract Task MkDir(string name);
public abstract Task<string> Pwd();
public abstract Task<string> DownloadUrl(FileData fileData);
public abstract Task<Stream> DownloadStream(FileData fileData);
public abstract Task Delete(FileData fileData);
public abstract Task Move(FileData fileData, string newPath);
public abstract Task Compress(params FileData[] files);
public abstract Task Decompress(FileData fileData);
public abstract Task<string> GetLaunchUrl();
public abstract object Clone();
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Helpers.Files;
public class FileData
{
public string Name { get; set; } = "";
public long Size { get; set; }
public bool IsFile { get; set; }
}

View file

@ -0,0 +1,232 @@
using System.Web;
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Wings.Requests;
using Moonlight.App.Models.Wings.Resources;
using Moonlight.App.Services;
using RestSharp;
namespace Moonlight.App.Helpers.Files;
public class WingsFileAccess : FileAccess
{
private readonly WingsApiHelper WingsApiHelper;
private readonly WingsJwtHelper WingsJwtHelper;
private readonly ConfigService ConfigService;
private readonly Server Server;
private readonly User User;
public WingsFileAccess(
WingsApiHelper wingsApiHelper,
WingsJwtHelper wingsJwtHelper,
Server server,
ConfigService configService,
User user)
{
WingsApiHelper = wingsApiHelper;
WingsJwtHelper = wingsJwtHelper;
Server = server;
ConfigService = configService;
User = user;
if (server.Node == null)
{
throw new ArgumentException("The wings file access server model needs to include the node data");
}
}
public override async Task<FileData[]> Ls()
{
var res = await WingsApiHelper.Get<ListDirectory[]>(
Server.Node,
$"api/servers/{Server.Uuid}/files/list-directory?directory={CurrentPath}"
);
var x = new List<FileData>();
foreach (var response in res)
{
x.Add(new()
{
Name = response.Name,
Size = response.File ? response.Size : 0,
IsFile = response.File,
});
}
return x.ToArray();
}
public override Task Cd(string dir)
{
var x = Path.Combine(CurrentPath, dir).Replace("\\", "/") + "/";
x = x.Replace("//", "/");
CurrentPath = x;
return Task.CompletedTask;
}
public override Task Up()
{
CurrentPath = Path.GetFullPath(Path.Combine(CurrentPath, "..")).Replace("\\", "/").Replace("C:", "");
return Task.CompletedTask;
}
public override Task SetDir(string dir)
{
CurrentPath = dir;
return Task.CompletedTask;
}
public override async Task<string> Read(FileData fileData)
{
return await WingsApiHelper.GetRaw(Server.Node,
$"api/servers/{Server.Uuid}/files/contents?file={CurrentPath}{fileData.Name}");
}
public override async Task Write(FileData fileData, string content)
{
await WingsApiHelper.PostRaw(Server.Node,
$"api/servers/{Server.Uuid}/files/write?file={CurrentPath}{fileData.Name}", content);
}
public override async Task Upload(string name, Stream dataStream, Action<int>? progressUpdated = null)
{
var token = WingsJwtHelper.Generate(
Server.Node.Token,
claims => { claims.Add("server_uuid", Server.Uuid.ToString()); }
);
var client = new RestClient();
var request = new RestRequest();
if (Server.Node.Ssl)
request.Resource =
$"https://{Server.Node.Fqdn}:{Server.Node.HttpPort}/upload/file?token={token}&directory={CurrentPath}";
else
request.Resource =
$"http://{Server.Node.Fqdn}:{Server.Node.HttpPort}/upload/file?token={token}&directory={CurrentPath}";
request.AddParameter("name", "files");
request.AddParameter("filename", name);
request.AddHeader("Content-Type", "multipart/form-data");
request.AddHeader("Origin", ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl"));
request.AddFile("files", () =>
{
return new StreamProgressHelper(dataStream)
{
Progress = i => { progressUpdated?.Invoke(i); }
};
}, name);
await client.ExecutePostAsync(request);
client.Dispose();
dataStream.Close();
}
public override async Task MkDir(string name)
{
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/create-directory",
new CreateDirectory()
{
Name = name,
Path = CurrentPath
}
);
}
public override Task<string> Pwd()
{
return Task.FromResult(CurrentPath);
}
public override Task<string> DownloadUrl(FileData fileData)
{
var token = WingsJwtHelper.Generate(Server.Node.Token, claims =>
{
claims.Add("server_uuid", Server.Uuid.ToString());
claims.Add("file_path", CurrentPath + "/" + fileData.Name);
});
if (Server.Node.Ssl)
{
return Task.FromResult(
$"https://{Server.Node.Fqdn}:{Server.Node.HttpPort}/download/file?token={token}"
);
}
else
{
return Task.FromResult(
$"http://{Server.Node.Fqdn}:{Server.Node.HttpPort}/download/file?token={token}"
);
}
}
public override Task<Stream> DownloadStream(FileData fileData)
{
throw new NotImplementedException();
}
public override async Task Delete(FileData fileData)
{
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/delete", new DeleteFiles()
{
Root = CurrentPath,
Files = new()
{
fileData.Name
}
});
}
public override async Task Move(FileData fileData, string newPath)
{
var req = new RenameFiles()
{
Root = "/",
Files = new[]
{
new RenameFilesData()
{
From = (CurrentPath + fileData.Name),
To = newPath
}
}
};
await WingsApiHelper.Put(Server.Node, $"api/servers/{Server.Uuid}/files/rename", req);
}
public override async Task Compress(params FileData[] files)
{
var req = new CompressFiles()
{
Root = CurrentPath,
Files = files.Select(x => x.Name).ToArray()
};
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/compress", req);
}
public override async Task Decompress(FileData fileData)
{
var req = new DecompressFile()
{
Root = CurrentPath,
File = fileData.Name
};
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req);
}
public override Task<string> GetLaunchUrl()
{
return Task.FromResult(
$"sftp://{User.Id}.{StringHelper.IntToStringWithLeadingZeros(Server.Id, 8)}@{Server.Node.Fqdn}:{Server.Node.SftpPort}");
}
public override object Clone()
{
return new WingsFileAccess(WingsApiHelper, WingsJwtHelper, Server, ConfigService, User);
}
}

View file

@ -24,11 +24,11 @@ public class PaperApiHelper
else
requrl = ApiUrl + "/" + url;
RestRequest request = new(requrl);
RestRequest request = new(requrl, Method.Get);
request.AddHeader("Content-Type", "application/json");
var response = await client.GetAsync(request);
var response = await client.ExecuteAsync(request);
if (!response.IsSuccessful)
{

View file

@ -14,24 +14,13 @@ public class WingsApiHelper
Client = new();
}
private string GetApiUrl(Node node)
{
if(node.Ssl)
return $"https://{node.Fqdn}:{node.HttpPort}/";
else
return $"http://{node.Fqdn}:{node.HttpPort}/";
//return $"https://{node.Fqdn}:{node.HttpPort}/";
}
public async Task<T> Get<T>(Node node, string resource)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Get;
var response = await Client.GetAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -53,13 +42,11 @@ public class WingsApiHelper
public async Task<string> GetRaw(Node node, string resource)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Get;
var response = await Client.GetAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -81,18 +68,16 @@ public class WingsApiHelper
public async Task<T> Post<T>(Node node, string resource, object? body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Post;
request.AddParameter("text/plain",
JsonConvert.SerializeObject(body),
ParameterType.RequestBody
);
var response = await Client.PostAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -114,16 +99,14 @@ public class WingsApiHelper
public async Task Post(Node node, string resource, object? body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Post;
if(body != null)
if(body != null)
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.PostAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -143,15 +126,13 @@ public class WingsApiHelper
public async Task PostRaw(Node node, string resource, object body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Post;
request.AddParameter("text/plain", body, ParameterType.RequestBody);
var response = await Client.PostAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -171,16 +152,14 @@ public class WingsApiHelper
public async Task Delete(Node node, string resource, object? body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Delete;
if(body != null)
if(body != null)
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.DeleteAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -200,15 +179,13 @@ public class WingsApiHelper
public async Task Put(Node node, string resource, object? body)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
request.Method = Method.Put;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.PutAsync(request);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@ -225,4 +202,20 @@ public class WingsApiHelper
}
}
}
private RestRequest CreateRequest(Node node, string resource)
{
var url = (node.Ssl ? "https" : "http") + $"://{node.Fqdn}:{node.HttpPort}/" + resource;
var request = new RestRequest(url)
{
Timeout = 60 * 15
};
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", "Bearer " + node.Token);
return request;
}
}

View file

@ -39,7 +39,7 @@ public class WingsFileAccess : IFileAccess
public async Task<FileManagerObject[]> GetDirectoryContent()
{
var res = await WingsApiHelper.Get<ListDirectoryRequest[]>(Node,
var res = await WingsApiHelper.Get<ListDirectory[]>(Node,
$"api/servers/{Server.Uuid}/files/list-directory?directory={Path}");
var x = new List<FileManagerObject>();
@ -130,7 +130,7 @@ public class WingsFileAccess : IFileAccess
public async Task CreateDirectory(string name)
{
await WingsApiHelper.Post(Node, $"api/servers/{Server.Uuid}/files/create-directory",
new CreateDirectoryRequest()
new CreateDirectory()
{
Name = name,
Path = Path
@ -171,7 +171,7 @@ public class WingsFileAccess : IFileAccess
public async Task Delete(FileManagerObject managerObject)
{
await WingsApiHelper.Post(Node, $"api/servers/{Server.Uuid}/files/delete", new DeleteFilesRequest()
await WingsApiHelper.Post(Node, $"api/servers/{Server.Uuid}/files/delete", new DeleteFiles()
{
Root = Path,
Files = new()
@ -183,7 +183,7 @@ public class WingsFileAccess : IFileAccess
public async Task Move(FileManagerObject managerObject, string newPath)
{
await WingsApiHelper.Put(Node, $"api/servers/{Server.Uuid}/files/rename", new RenameFilesRequest()
await WingsApiHelper.Put(Node, $"api/servers/{Server.Uuid}/files/rename", new RenameFiles()
{
Root = "/",
Files = new[]

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Models.Files;
public class FileContextAction
{
public string Id { get; set; }
public string Name { get; set; }
public Action<FileManagerObject> Action { get; set; }
}

View file

@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Moonlight.App.Models.Wings.Requests;
public class CompressFiles
{
[JsonProperty("root")]
public string Root { get; set; }
[JsonProperty("files")]
public string[] Files { get; set; }
}

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class CreateBackupRequest
public class CreateBackup
{
[JsonProperty("adapter")]
public string Adapter { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class CreateDirectoryRequest
public class CreateDirectory
{
[JsonProperty("name")]
public string Name { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class CreateServerRequest
public class CreateServer
{
[JsonProperty("uuid")]
public Guid Uuid { get; set; }

View file

@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Moonlight.App.Models.Wings.Requests;
public class DecompressFile
{
[JsonProperty("root")]
public string Root { get; set; }
[JsonProperty("file")]
public string File { get; set; }
}

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class DeleteFilesRequest
public class DeleteFiles
{
[JsonProperty("root")]
public string Root { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class RenameFilesRequest
public class RenameFiles
{
[JsonProperty("root")]
public string Root { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class RestoreBackupRequest
public class RestoreBackup
{
[JsonProperty("adapter")]
public string Adapter { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Requests;
public class ServerPowerRequest
public class ServerPower
{
[JsonProperty("action")]
public string Action { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Resources;
public class ListDirectoryRequest
public class ListDirectory
{
[JsonProperty("name")]
public string Name { get; set; }

View file

@ -2,7 +2,7 @@
namespace Moonlight.App.Models.Wings.Resources;
public class ServerDetailsResponse
public class ServerDetails
{
[JsonProperty("state")]
public string State { get; set; }
@ -11,9 +11,9 @@ public class ServerDetailsResponse
public bool IsSuspended { get; set; }
[JsonProperty("utilization")]
public ServerDetailsResponseUtilization Utilization { get; set; }
public ServerDetailsUtilization Utilization { get; set; }
public class ServerDetailsResponseUtilization
public class ServerDetailsUtilization
{
[JsonProperty("memory_bytes")]
public long MemoryBytes { get; set; }
@ -25,7 +25,7 @@ public class ServerDetailsResponse
public double CpuAbsolute { get; set; }
[JsonProperty("network")]
public ServerDetailsResponseNetwork Network { get; set; }
public ServerDetailsNetwork Network { get; set; }
[JsonProperty("uptime")]
public long Uptime { get; set; }
@ -37,7 +37,7 @@ public class ServerDetailsResponse
public long DiskBytes { get; set; }
}
public class ServerDetailsResponseNetwork
public class ServerDetailsNetwork
{
[JsonProperty("rx_bytes")]
public long RxBytes { get; set; }

View file

@ -0,0 +1,23 @@
using Microsoft.JSInterop;
namespace Moonlight.App.Services.Interop;
public class ModalService
{
private readonly IJSRuntime JsRuntime;
public ModalService(IJSRuntime jsRuntime)
{
JsRuntime = jsRuntime;
}
public async Task Show(string name)
{
await JsRuntime.InvokeVoidAsync("moonlight.modals.show", name);
}
public async Task Hide(string name)
{
await JsRuntime.InvokeVoidAsync("moonlight.modals.hide", name);
}
}

View file

@ -30,4 +30,19 @@ public class ToastService
{
await JsRuntime.InvokeVoidAsync("showSuccessToast", message);
}
public async Task CreateProcessToast(string id, string text)
{
await JsRuntime.InvokeVoidAsync("createToast", id, text);
}
public async Task UpdateProcessToast(string id, string text)
{
await JsRuntime.InvokeVoidAsync("modifyToast", id, text);
}
public async Task RemoveProcessToast(string id)
{
await JsRuntime.InvokeVoidAsync("removeToast", id);
}
}

View file

@ -75,11 +75,11 @@ public class ServerService
return s;
}
public async Task<ServerDetailsResponse> GetDetails(Server s)
public async Task<ServerDetails> GetDetails(Server s)
{
Server server = EnsureNodeData(s);
return await WingsApiHelper.Get<ServerDetailsResponse>(
return await WingsApiHelper.Get<ServerDetails>(
server.Node,
$"api/servers/{server.Uuid}"
);
@ -91,7 +91,7 @@ public class ServerService
var rawSignal = signal.ToString().ToLower();
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPowerRequest()
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPower()
{
Action = rawSignal
});
@ -118,7 +118,7 @@ public class ServerService
serverData.Backups.Add(backup);
ServerRepository.Update(serverData);
await WingsApiHelper.Post(serverData.Node, $"api/servers/{serverData.Uuid}/backup", new CreateBackupRequest()
await WingsApiHelper.Post(serverData.Node, $"api/servers/{serverData.Uuid}/backup", new CreateBackup()
{
Adapter = "wings",
Uuid = backup.Uuid,
@ -158,7 +158,7 @@ public class ServerService
Server server = EnsureNodeData(s);
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/backup/{serverBackup.Uuid}/restore",
new RestoreBackupRequest()
new RestoreBackup()
{
Adapter = "wings"
});
@ -299,7 +299,7 @@ public class ServerService
try
{
await WingsApiHelper.Post(node, $"api/servers", new CreateServerRequest()
await WingsApiHelper.Post(node, $"api/servers", new CreateServer()
{
Uuid = newServerData.Uuid,
StartOnCompletion = false

View file

@ -68,7 +68,6 @@
<Folder Include="App\Models\Google\Resources" />
<Folder Include="App\Services\DiscordBot\Modules" />
<Folder Include="resources\lang" />
<Folder Include="wwwroot\assets\media" />
</ItemGroup>
</Project>

View file

@ -101,6 +101,7 @@
<script>require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });</script>
<script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="/_content/BlazorMonaco/jsInterop.js"></script>
<script src="/assets/js/monacoTheme.js"></script>
<script src="/assets/js/scripts.bundle.js"></script>
<script src="/assets/js/flashbang.js"></script>
@ -112,5 +113,6 @@
<script src="/assets/js/loggingUtils.js"></script>
<script src="/assets/js/snow.js"></script>
<script src="/assets/js/recaptcha.js"></script>
<script src="/assets/js/moonlight.js"></script>
</body>
</html>

View file

@ -93,6 +93,7 @@ namespace Moonlight
builder.Services.AddSingleton<NotificationServerService>();
builder.Services.AddScoped<NotificationAdminService>();
builder.Services.AddScoped<NotificationClientService>();
builder.Services.AddScoped<ModalService>();
builder.Services.AddScoped<GoogleOAuth2Service>();
builder.Services.AddScoped<DiscordOAuth2Service>();

View file

@ -3,27 +3,30 @@
@using Moonlight.Shared.Components.Partials
@inject SmartTranslateService TranslationService
@inject IJSRuntime JsRuntime
<div class="card-body">
<MonacoEditor CssClass="h-100" @ref="Editor" Id="vseditor" ConstructionOptions="(x) => EditorOptions"/>
</div>
@if (!HideControls)
{
<div class="card-footer pt-0">
<div class="btn-group">
<WButton
Text="@(TranslationService.Translate("Save"))"
WorkingText="@(TranslationService.Translate("Saving"))"
OnClick="Submit"></WButton>
<WButton
CssClasses="btn-danger"
Text="@(TranslationService.Translate("Cancel"))"
WorkingText="@(TranslationService.Translate("Canceling"))"
OnClick="Cancel"></WButton>
</div>
<div class="card bg-black rounded">
<div class="card-body">
<MonacoEditor CssClass="h-100" @ref="Editor" Id="vseditor" ConstructionOptions="(x) => EditorOptions"/>
</div>
}
@if (!HideControls)
{
<div class="card-footer">
<div class="btn-group">
<WButton
Text="@(TranslationService.Translate("Save"))"
WorkingText="@(TranslationService.Translate("Saving"))"
OnClick="Submit"></WButton>
<WButton
CssClasses="btn-danger"
Text="@(TranslationService.Translate("Cancel"))"
WorkingText="@(TranslationService.Translate("Canceling"))"
OnClick="Cancel"></WButton>
</div>
</div>
}
</div>
@code
{
@ -53,8 +56,8 @@
{
AutomaticLayout = true,
Language = "plaintext",
Value = "Wird geladen",
Theme = "vs-dark",
Value = "Loading content",
Theme = "moonlight-theme",
Contextmenu = false,
Minimap = new()
{
@ -68,6 +71,8 @@
{
if (firstRender)
{
await JsRuntime.InvokeVoidAsync("initMonacoTheme");
Editor.OnDidInit = new EventCallback<MonacoEditorBase>(this, async () =>
{
EditorOptions.Language = Language;

View file

@ -1,529 +1,329 @@
@using Moonlight.App.Helpers
@using BlazorContextMenu
@using BlazorDownloadFile
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Helpers
@using Logging.Net
@using Moonlight.App.Models.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using BlazorDownloadFile
@inject AlertService AlertService
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject SmartTranslateService TranslationService
@inject AlertService AlertService
@inject SmartTranslateService SmartTranslateService
@inject IBlazorDownloadFileService FileService
<div class="card card-flush">
@if (Editing == null)
@if (Editing)
{
<div class="card-header pt-8">
<div class="card-title">
<div class="d-flex align-items-center position-relative my-1">
<span class="svg-icon svg-icon-1 position-absolute ms-6">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="17.0365" y="15.1223" width="8.15546" height="2" rx="1" transform="rotate(45 17.0365 15.1223)" fill="currentColor"></rect>
<path d="M11 19C6.55556 19 3 15.4444 3 11C3 6.55556 6.55556 3 11 3C15.4444 3 19 6.55556 19 11C19 15.4444 15.4444 19 11 19ZM11 5C7.53333 5 5 7.53333 5 11C5 14.4667 7.53333 17 11 17C14.4667 17 17 14.4667 17 11C17 7.53333 14.4667 5 11 5Z" fill="currentColor"></path>
</svg>
</span>
<input type="text" @bind="Search" @bind:event="oninput" class="form-control form-control-solid w-250px ps-15" placeholder="@(TranslationService.Translate("Search files and folders"))">
</div>
</div>
<div class="card-toolbar">
@if (SelectedFiles.Count == 0)
{
<div class="d-flex justify-content-end">
<button type="button" @onclick="Launch" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-muted svg-icon-2hx">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M5 16C3.3 16 2 14.7 2 13C2 11.3 3.3 10 5 10H5.1C5 9.7 5 9.3 5 9C5 6.2 7.2 4 10 4C11.9 4 13.5 5 14.3 6.5C14.8 6.2 15.4 6 16 6C17.7 6 19 7.3 19 9C19 9.4 18.9 9.7 18.8 10C18.9 10 18.9 10 19 10C20.7 10 22 11.3 22 13C22 14.7 20.7 16 19 16H5ZM8 13.6H16L12.7 10.3C12.3 9.89999 11.7 9.89999 11.3 10.3L8 13.6Z" fill="currentColor"/>
<path d="M11 13.6V19C11 19.6 11.4 20 12 20C12.6 20 13 19.6 13 19V13.6H11Z" fill="currentColor"/>
</svg>
</span>
<TL>Launch WinSCP</TL>
</button>
<button type="button" @onclick="CreateFolder" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.2C9.7 3 10.2 3.20001 10.4 3.60001ZM16 12H13V9C13 8.4 12.6 8 12 8C11.4 8 11 8.4 11 9V12H8C7.4 12 7 12.4 7 13C7 13.6 7.4 14 8 14H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 14H8C7.4 14 7 13.6 7 13C7 12.4 7.4 12 8 12H11V14ZM16 12H13V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
</svg>
</span>
<TL>New folder</TL>
</button>
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden="" multiple=""/>
<label for="fileUpload" class="btn btn-primary me-3 pt-5 @(Uploading ? "disabled" : "")">
@if (Uploading)
{
<span>@(Percent)%</span>
}
else
{
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
</svg>
</span>
<TL>Upload</TL>
}
</label>
</div>
}
else
{
<div class="d-flex justify-content-end align-items-center">
<div class="fw-bold me-5">
<span class="me-2">
@(SelectedFiles.Count)
</span>
<TL>Selected</TL>
</div>
<button type="button" class="btn btn-primary me-3">
<TL>Move deleted</TL>
</button>
<button type="button" class="btn btn-danger">
<TL>Delete selected</TL>
</button>
</div>
}
</div>
</div>
<div class="card-body">
<div class="d-flex flex-stack">
<div class="badge badge-lg badge-light-primary">
<div class="d-flex align-items-center flex-wrap">
@{
var vx = "/";
}
<a @onclick:preventDefault @onclick="() => SetPath(vx)" href="#">/</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
@{
var cp = "/";
var lp = "/";
var pathParts = CurrentPath.Replace("\\", "/").Split('/', StringSplitOptions.RemoveEmptyEntries);
foreach (var path in pathParts)
{
lp = cp;
<a @onclick:preventDefault @onclick="() => SetPath(lp)" href="#">@(path)</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
cp += path + "/";
}
}
</div>
</div>
</div>
<div class="dt-bootstrap4 no-footer">
@if (Loading)
{
<div class="mt-5 alert alert-info">
<span>
<TL>Loading</TL> <span class="spinner-grow align-middle ms-2"></span>
</span>
</div>
}
else
{
<div class="table-responsive">
<table class="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer">
<thead>
<tr class="text-start text-gray-400 fw-bold fs-7 text-uppercase gs-0">
<th class="w-10px pe-2">
<div class="form-check form-check-sm form-check-custom form-check-solid me-3">
<input class="form-check-input" type="checkbox" @onchange="OnAllFileToggle">
</div>
</th>
<th class="min-w-250px">
<TL>File name</TL>
</th>
<th class="min-w-10px">
<TL>File size</TL>
</th>
<th class="min-w-125px">
<TL>Last modified</TL>
</th>
<th class="w-125px"></th>
</tr>
</thead>
<tbody class="fw-semibold text-gray-600">
@foreach (var obj in Objects.Where(x => x.Name.Contains(Search)))
{
<tr class="odd">
<td>
<div class="form-check form-check-sm form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" @onchange="(e) => OnFileToggle(e, obj)">
</div>
</td>
<td>
<div class="d-flex align-items-center">
@if (obj.IsFile)
{
<span class="svg-icon svg-icon-2x svg-icon-primary me-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M19 22H5C4.4 22 4 21.6 4 21V3C4 2.4 4.4 2 5 2H14L20 8V21C20 21.6 19.6 22 19 22Z" fill="currentColor"></path>
<path d="M15 8H20L14 2V7C14 7.6 14.4 8 15 8Z" fill="currentColor"></path>
</svg>
</span>
}
else
{
<span class="svg-icon svg-icon-2x svg-icon-primary me-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M9.2 3H3C2.4 3 2 3.4 2 4V19C2 19.6 2.4 20 3 20H21C21.6 20 22 19.6 22 19V7C22 6.4 21.6 6 21 6H12L10.4 3.60001C10.2 3.20001 9.7 3 9.2 3Z" fill="currentColor"></path>
</svg>
</span>
}
@if (obj.IsFile)
{
<a href="#" @onclick:preventDefault @onclick="() => OpenFile(obj)" class="text-gray-800 text-hover-primary">@(obj.Name)</a>
}
else
{
<a href="#" @onclick:preventDefault @onclick="() => CdPath(obj.Name)" class="text-gray-800 text-hover-primary">@(obj.Name)</a>
}
</div>
</td>
<td>
@(Formatter.FormatSize(obj.Size))
</td>
<td>
@(obj.UpdatedAt.ToShortDateString()) @(obj.UpdatedAt.ToShortTimeString())
</td>
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2">
<ContextMenuTrigger MenuId="triggerMenu" MouseButtonTrigger="MouseButtonTrigger.Both" Data="obj">
<button class="btn btn-sm btn-icon btn-light btn-active-light-primary me-2">
<span class="svg-icon svg-icon-5 m-0">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="17" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="3" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
</svg>
</span>
</button>
</ContextMenuTrigger>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
<FileEditor @ref="Editor"
InitialData="@EditorInitialData"
Language="@EditorLanguage"
OnCancel="() => Cancel()"
OnSubmit="(_) => Cancel(true)"
HideControls="false">
</FileEditor>
}
else
{
if (Loading)
{
<div class="mt-5 alert alert-info">
<span>
<TL>Loading</TL> <span class="spinner-grow align-middle ms-2"></span>
</span>
</div>
}
else
{
<FileEditor OnSubmit="SaveFile" OnCancel="CloseFile" InitialData="@(InitialEditorData)" Language="@(Language)"></FileEditor>
}
}
</div>
<div class="card mb-7">
<div class="card-header">
<div class="card-title">
<div class="d-flex flex-stack">
<FilePath Access="Access" OnPathChanged="OnComponentStateChanged" />
</div>
</div>
<div class="card-toolbar">
<div class="d-flex justify-content-end align-items-center">
@if (View != null && View.SelectedFiles.Any())
{
<div class="fw-bold me-5">
<span class="me-2">@(View.SelectedFiles.Length) <TL>selected</TL></span>
</div>
<WButton Text="@(SmartTranslateService.Translate("Move"))"
WorkingText="@(SmartTranslateService.Translate("Moving"))"
CssClasses="btn-primary me-3"
OnClick="StartMoveFiles">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Compress"))"
WorkingText="@(SmartTranslateService.Translate("Compressing"))"
CssClasses="btn-primary me-3"
OnClick="CompressMultiple">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Delete"))"
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
CssClasses="btn-danger"
OnClick="DeleteMultiple">
</WButton>
}
else
{
<button type="button" @onclick="Launch" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-muted svg-icon-2hx">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M5 16C3.3 16 2 14.7 2 13C2 11.3 3.3 10 5 10H5.1C5 9.7 5 9.3 5 9C5 6.2 7.2 4 10 4C11.9 4 13.5 5 14.3 6.5C14.8 6.2 15.4 6 16 6C17.7 6 19 7.3 19 9C19 9.4 18.9 9.7 18.8 10C18.9 10 18.9 10 19 10C20.7 10 22 11.3 22 13C22 14.7 20.7 16 19 16H5ZM8 13.6H16L12.7 10.3C12.3 9.89999 11.7 9.89999 11.3 10.3L8 13.6Z" fill="currentColor"/>
<path d="M11 13.6V19C11 19.6 11.4 20 12 20C12.6 20 13 19.6 13 19V13.6H11Z" fill="currentColor"/>
</svg>
</span>
<TL>Launch WinSCP</TL>
</button>
<ContextMenu Id="triggerMenu" CssClass="bg-secondary z-10">
<Item Id="rename" OnClick="OnContextMenuClick"><TL>Rename</TL></Item>
<Item Id="move" OnClick="OnContextMenuClick"><TL>Move</TL></Item>
<Item Id="archive" OnClick="OnContextMenuClick"><TL>Archive</TL></Item>
<Item Id="unarchive" OnClick="OnContextMenuClick"><TL>Unarchive</TL></Item>
<Item Id="download" OnClick="OnContextMenuClick"><TL>Download</TL></Item>
<Item Id="delete" OnClick="OnContextMenuClick"><TL>Delete</TL></Item>
</ContextMenu>
<button type="button" @onclick="CreateFolder" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.2C9.7 3 10.2 3.20001 10.4 3.60001ZM16 12H13V9C13 8.4 12.6 8 12 8C11.4 8 11 8.4 11 9V12H8C7.4 12 7 12.4 7 13C7 13.6 7.4 14 8 14H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 14H8C7.4 14 7 13.6 7 13C7 12.4 7.4 12 8 12H11V14ZM16 12H13V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
</svg>
</span>
<TL>New folder</TL>
</button>
<FileUpload Access="Access" OnUploadComplete="OnComponentStateChanged" />
}
</div>
</div>
</div>
</div>
<div class="card card-body">
<FileView @ref="View"
Access="Access"
ContextActions="Actions"
OnSelectionChanged="OnSelectionChanged"
OnElementClicked="OnElementClicked"
DisableScrolling="true">
</FileView>
</div>
<FileSelectModal @ref="FileSelectModal"
OnlyFolder="true"
Title="@(SmartTranslateService.Translate("Select folder to move the file(s) to"))"
Access="MoveAccess"
OnSubmit="OnFileMoveSubmit">
</FileSelectModal>
}
@code
{
[Parameter]
public IFileAccess FileAccess { get; set; }
public FileAccess Access { get; set; }
// Data
// File Editor
private bool Editing = false;
private string EditorInitialData = "";
private string EditorLanguage = "";
private FileData EditingFile;
private FileEditor Editor;
private List<FileManagerObject> SelectedFiles { get; set; } = new();
private List<FileManagerObject> Objects { get; set; } = new();
private string CurrentPath = "";
// File View
private FileView? View;
// Search
private string SearchValue = "";
// File Move
private FileAccess MoveAccess;
private FileSelectModal FileSelectModal;
private FileData? SingleMoveFile = null;
// Config
private ContextAction[] Actions = Array.Empty<ContextAction>();
private string Search
protected override void OnInitialized()
{
get { return SearchValue; }
set
MoveAccess = (FileAccess)Access.Clone();
List<ContextAction> actions = new();
actions.Add(new()
{
SearchValue = value;
InvokeAsync(StateHasChanged);
}
}
Id = "rename",
Name = "Rename",
Action = async (x) =>
{
var name = await AlertService.Text(
SmartTranslateService.Translate("Rename"),
SmartTranslateService.Translate("Enter a new name"),
x.Name
);
// States
private bool Loading = false;
// States - Editor
private FileManagerObject? Editing = null;
private string InitialEditorData = "";
private string Language = "plaintext";
// States - File Upload
private bool Uploading = false;
private int Percent = 0;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
if (name != x.Name)
{
await Access.Move(x, Access.CurrentPath + name);
}
await View!.Refresh();
}
});
actions.Add(new ()
{
await RefreshActive();
}
}
private async Task RefreshActive()
{
Loading = true;
await InvokeAsync(StateHasChanged);
await Refresh(false);
Loading = false;
await InvokeAsync(StateHasChanged);
}
private async Task Refresh(bool rerender = true)
{
SelectedFiles.Clear();
Objects.Clear();
CurrentPath = await FileAccess.GetCurrentPath();
var data = await FileAccess.GetDirectoryContent();
Objects = data.ToList();
if (rerender)
await InvokeAsync(StateHasChanged);
}
private async Task CdPath(string path)
{
await FileAccess.ChangeDirectory(path);
await RefreshActive();
}
private async Task SetPath(string path)
{
await FileAccess.SetDirectory(path);
await RefreshActive();
}
private async Task OnContextMenuClick(ItemClickEventArgs e)
{
var data = e.Data as FileManagerObject;
if (data == null)
return;
switch (e.MenuItem.Id)
{
case "delete":
await FileAccess.Delete(data);
break;
case "download":
if (data.IsFile)
Id = "download",
Name = "Download",
Action = async (x) =>
{
if (x.IsFile)
{
try
{
var stream = await FileAccess.GetDownloadStream(data);
await ToastService.Info(TranslationService.Translate("Starting download"));
var stream = await Access.DownloadStream(x);
await ToastService.Info(SmartTranslateService.Translate("Starting download"));
await FileService.AddBuffer(stream);
await FileService.DownloadBinaryBuffers(data.Name);
await FileService.DownloadBinaryBuffers(x.Name);
}
catch (NotImplementedException)
{
try
{
var url = await FileAccess.GetDownloadUrl(data);
NavigationManager.NavigateTo(url, true);
await ToastService.Info(TranslationService.Translate("Starting download"));
}
catch (Exception exception)
{
await ToastService.Error(TranslationService.Translate("Error starting download"));
Logger.Error("Error downloading file");
Logger.Error(exception.Message);
}
}
catch (Exception exception)
{
await ToastService.Error(TranslationService.Translate("Error starting download"));
Logger.Error("Error downloading file stream");
Logger.Error(exception.Message);
var url = await Access.DownloadUrl(x);
NavigationManager.NavigateTo(url, true);
await ToastService.Info(SmartTranslateService.Translate("Starting download"));
}
}
break;
case "rename":
var newName = await AlertService.Text(TranslationService.Translate("Rename"), TranslationService.Translate("Enter a new name"), data.Name);
var path = await FileAccess.GetCurrentPath();
await FileAccess.Move(data, path + "/" + newName);
break;
}
await Refresh(false);
}
private async Task OnFileToggle(ChangeEventArgs obj, FileManagerObject o)
{
if ((bool)obj.Value)
{
if (SelectedFiles.Contains(o))
return;
SelectedFiles.Add(o);
await InvokeAsync(StateHasChanged);
}
else
{
if (!SelectedFiles.Contains(o))
return;
SelectedFiles.Remove(o);
await InvokeAsync(StateHasChanged);
}
}
private async Task OnAllFileToggle(ChangeEventArgs obj)
{
if ((bool)obj.Value)
{
foreach (var o in Objects)
{
if (SelectedFiles.Contains(o))
continue;
SelectedFiles.Add(o);
}
await InvokeAsync(StateHasChanged);
}
else
});
actions.Add(new()
{
foreach (var o in Objects)
Id = "compress",
Name = "Compress",
Action = async (x) =>
{
if (!SelectedFiles.Contains(o))
continue;
SelectedFiles.Remove(o);
await Access.Compress(x);
await View!.Refresh();
}
});
actions.Add(new ()
{
Id = "decompress",
Name = "Decompress",
Action = async (x) =>
{
await Access.Decompress(x);
await View!.Refresh();
}
});
actions.Add(new()
{
Id = "move",
Name = "Move",
Action = async (x) =>
{
SingleMoveFile = x;
await StartMoveFiles();
}
});
actions.Add(new()
{
Id = "delete",
Name = "Delete",
Action = async (x) =>
{
await Access.Delete(x);
await View!.Refresh();
}
});
Actions = actions.ToArray();
}
private async Task<bool> OnElementClicked(FileData fileData)
{
if (fileData.IsFile)
{
EditorInitialData = await Access.Read(fileData);
EditorLanguage = MonacoTypeHelper.GetEditorType(fileData.Name);
EditingFile = fileData;
Editing = true;
await InvokeAsync(StateHasChanged);
return true;
}
}
private async Task CreateFolder()
{
var name = await AlertService.Text(TranslationService.Translate("Create a new folder"), TranslationService.Translate("Enter a name"), "");
if (string.IsNullOrEmpty(name))
return;
await FileAccess.CreateDirectory(name);
await Refresh();
}
private async void SaveFile(string data)
{
if (Editing == null)
return;
await FileAccess.WriteFile(Editing, data);
Editing = null;
await Refresh();
}
private async Task OpenFile(FileManagerObject o)
{
Editing = o;
Loading = true;
await InvokeAsync(StateHasChanged);
InitialEditorData = await FileAccess.ReadFile(Editing);
Language = MonacoTypeHelper.GetEditorType(Editing.Name);
Loading = false;
await InvokeAsync(StateHasChanged);
return false;
}
private async void CloseFile()
private async void Cancel(bool save = false)
{
Editing = null;
if (save)
{
var data = await Editor.GetData();
await Access.Write(EditingFile, data);
}
Editing = false;
await InvokeAsync(StateHasChanged);
}
private async Task Launch()
{
NavigationManager.NavigateTo(await FileAccess.GetLaunchUrl());
var url = await Access.GetLaunchUrl();
NavigationManager.NavigateTo(url, true);
}
private async Task OnFileChanged(InputFileChangeEventArgs arg)
private async Task CreateFolder()
{
var name = await AlertService.Text(
SmartTranslateService.Translate("Create a new folder"),
SmartTranslateService.Translate("Enter a name"),
""
);
if (string.IsNullOrEmpty(name))
return;
await Access.MkDir(name);
await View!.Refresh();
}
private async Task OnSelectionChanged()
{
Uploading = true;
await InvokeAsync(StateHasChanged);
}
foreach (var browserFile in arg.GetMultipleFiles())
private async Task StartMoveFiles()
{
await FileSelectModal.Show();
}
private async Task DeleteMultiple()
{
foreach (var data in View!.SelectedFiles)
{
if (browserFile.Size < 1024 * 1024 * 100)
{
Percent = 0;
try
{
await FileAccess.UploadFile(
browserFile.Name,
browserFile.OpenReadStream(1024 * 1024 * 100),
async (i) =>
{
Percent = i;
Task.Run(() => { InvokeAsync(StateHasChanged); });
});
await Refresh();
}
catch (Exception e)
{
await ToastService.Error(TranslationService.Translate("An unknown error occured while uploading a file"));
Logger.Error("Error uploading file");
Logger.Error(e);
}
}
else
{
await ToastService.Error(TranslationService.Translate("The uploaded file should not be bigger than 100MB"));
}
await Access.Delete(data);
}
Uploading = false;
await InvokeAsync(StateHasChanged);
await View!.Refresh();
}
await ToastService.Success(TranslationService.Translate("File upload complete"));
private async Task CompressMultiple()
{
await Access.Compress(View!.SelectedFiles);
await View!.Refresh();
}
private async Task OnFileMoveSubmit(string path)
{
foreach (var sFile in View!.SelectedFiles)
{
await Access.Move(sFile, path + sFile.Name);
}
if (SingleMoveFile != null)
{
await Access.Move(SingleMoveFile, path + SingleMoveFile.Name);
SingleMoveFile = null;
}
await View.Refresh();
}
// This method can be called by every component to refresh the view
private async Task OnComponentStateChanged()
{
await View!.Refresh();
await InvokeAsync(StateHasChanged);
}
}

View file

@ -0,0 +1,529 @@
@using Moonlight.App.Helpers
@using BlazorContextMenu
@using BlazorDownloadFile
@using Logging.Net
@using Moonlight.App.Models.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject AlertService AlertService
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject SmartTranslateService TranslationService
@inject IBlazorDownloadFileService FileService
<div class="card card-flush">
@if (Editing == null)
{
<div class="card-header pt-8">
<div class="card-title">
<div class="d-flex align-items-center position-relative my-1">
<span class="svg-icon svg-icon-1 position-absolute ms-6">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="17.0365" y="15.1223" width="8.15546" height="2" rx="1" transform="rotate(45 17.0365 15.1223)" fill="currentColor"></rect>
<path d="M11 19C6.55556 19 3 15.4444 3 11C3 6.55556 6.55556 3 11 3C15.4444 3 19 6.55556 19 11C19 15.4444 15.4444 19 11 19ZM11 5C7.53333 5 5 7.53333 5 11C5 14.4667 7.53333 17 11 17C14.4667 17 17 14.4667 17 11C17 7.53333 14.4667 5 11 5Z" fill="currentColor"></path>
</svg>
</span>
<input type="text" @bind="Search" @bind:event="oninput" class="form-control form-control-solid w-250px ps-15" placeholder="@(TranslationService.Translate("Search files and folders"))">
</div>
</div>
<div class="card-toolbar">
@if (SelectedFiles.Count == 0)
{
<div class="d-flex justify-content-end">
<button type="button" @onclick="Launch" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-muted svg-icon-2hx">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M5 16C3.3 16 2 14.7 2 13C2 11.3 3.3 10 5 10H5.1C5 9.7 5 9.3 5 9C5 6.2 7.2 4 10 4C11.9 4 13.5 5 14.3 6.5C14.8 6.2 15.4 6 16 6C17.7 6 19 7.3 19 9C19 9.4 18.9 9.7 18.8 10C18.9 10 18.9 10 19 10C20.7 10 22 11.3 22 13C22 14.7 20.7 16 19 16H5ZM8 13.6H16L12.7 10.3C12.3 9.89999 11.7 9.89999 11.3 10.3L8 13.6Z" fill="currentColor"/>
<path d="M11 13.6V19C11 19.6 11.4 20 12 20C12.6 20 13 19.6 13 19V13.6H11Z" fill="currentColor"/>
</svg>
</span>
<TL>Launch WinSCP</TL>
</button>
<button type="button" @onclick="CreateFolder" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.2C9.7 3 10.2 3.20001 10.4 3.60001ZM16 12H13V9C13 8.4 12.6 8 12 8C11.4 8 11 8.4 11 9V12H8C7.4 12 7 12.4 7 13C7 13.6 7.4 14 8 14H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 14H8C7.4 14 7 13.6 7 13C7 12.4 7.4 12 8 12H11V14ZM16 12H13V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
</svg>
</span>
<TL>New folder</TL>
</button>
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden="" multiple=""/>
<label for="fileUpload" class="btn btn-primary me-3 pt-5 @(Uploading ? "disabled" : "")">
@if (Uploading)
{
<span>@(Percent)%</span>
}
else
{
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
</svg>
</span>
<TL>Upload</TL>
}
</label>
</div>
}
else
{
<div class="d-flex justify-content-end align-items-center">
<div class="fw-bold me-5">
<span class="me-2">
@(SelectedFiles.Count)
</span>
<TL>Selected</TL>
</div>
<button type="button" class="btn btn-primary me-3">
<TL>Move deleted</TL>
</button>
<button type="button" class="btn btn-danger">
<TL>Delete selected</TL>
</button>
</div>
}
</div>
</div>
<div class="card-body">
<div class="d-flex flex-stack">
<div class="badge badge-lg badge-light-primary">
<div class="d-flex align-items-center flex-wrap">
@{
var vx = "/";
}
<a @onclick:preventDefault @onclick="() => SetPath(vx)" href="#">/</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
@{
var cp = "/";
var lp = "/";
var pathParts = CurrentPath.Replace("\\", "/").Split('/', StringSplitOptions.RemoveEmptyEntries);
foreach (var path in pathParts)
{
lp = cp;
<a @onclick:preventDefault @onclick="() => SetPath(lp)" href="#">@(path)</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
cp += path + "/";
}
}
</div>
</div>
</div>
<div class="dt-bootstrap4 no-footer">
@if (Loading)
{
<div class="mt-5 alert alert-info">
<span>
<TL>Loading</TL> <span class="spinner-grow align-middle ms-2"></span>
</span>
</div>
}
else
{
<div class="table-responsive">
<table class="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer">
<thead>
<tr class="text-start text-gray-400 fw-bold fs-7 text-uppercase gs-0">
<th class="w-10px pe-2">
<div class="form-check form-check-sm form-check-custom form-check-solid me-3">
<input class="form-check-input" type="checkbox" @onchange="OnAllFileToggle">
</div>
</th>
<th class="min-w-250px">
<TL>File name</TL>
</th>
<th class="min-w-10px">
<TL>File size</TL>
</th>
<th class="min-w-125px">
<TL>Last modified</TL>
</th>
<th class="w-125px"></th>
</tr>
</thead>
<tbody class="fw-semibold text-gray-600">
@foreach (var obj in Objects.Where(x => x.Name.Contains(Search)))
{
<tr class="odd">
<td>
<div class="form-check form-check-sm form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" @onchange="(e) => OnFileToggle(e, obj)">
</div>
</td>
<td>
<div class="d-flex align-items-center">
@if (obj.IsFile)
{
<span class="svg-icon svg-icon-2x svg-icon-primary me-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M19 22H5C4.4 22 4 21.6 4 21V3C4 2.4 4.4 2 5 2H14L20 8V21C20 21.6 19.6 22 19 22Z" fill="currentColor"></path>
<path d="M15 8H20L14 2V7C14 7.6 14.4 8 15 8Z" fill="currentColor"></path>
</svg>
</span>
}
else
{
<span class="svg-icon svg-icon-2x svg-icon-primary me-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M9.2 3H3C2.4 3 2 3.4 2 4V19C2 19.6 2.4 20 3 20H21C21.6 20 22 19.6 22 19V7C22 6.4 21.6 6 21 6H12L10.4 3.60001C10.2 3.20001 9.7 3 9.2 3Z" fill="currentColor"></path>
</svg>
</span>
}
@if (obj.IsFile)
{
<a href="#" @onclick:preventDefault @onclick="() => OpenFile(obj)" class="text-gray-800 text-hover-primary">@(obj.Name)</a>
}
else
{
<a href="#" @onclick:preventDefault @onclick="() => CdPath(obj.Name)" class="text-gray-800 text-hover-primary">@(obj.Name)</a>
}
</div>
</td>
<td>
@(Formatter.FormatSize(obj.Size))
</td>
<td>
@(obj.UpdatedAt.ToShortDateString()) @(obj.UpdatedAt.ToShortTimeString())
</td>
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2">
<ContextMenuTrigger MenuId="triggerMenu" MouseButtonTrigger="MouseButtonTrigger.Both" Data="obj">
<button class="btn btn-sm btn-icon btn-light btn-active-light-primary me-2">
<span class="svg-icon svg-icon-5 m-0">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="17" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="3" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
</svg>
</span>
</button>
</ContextMenuTrigger>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
}
else
{
if (Loading)
{
<div class="mt-5 alert alert-info">
<span>
<TL>Loading</TL> <span class="spinner-grow align-middle ms-2"></span>
</span>
</div>
}
else
{
<FileEditor OnSubmit="SaveFile" OnCancel="CloseFile" InitialData="@(InitialEditorData)" Language="@(Language)"></FileEditor>
}
}
</div>
<ContextMenu Id="triggerMenu" CssClass="bg-secondary z-10">
<Item Id="rename" OnClick="OnContextMenuClick"><TL>Rename</TL></Item>
<Item Id="move" OnClick="OnContextMenuClick"><TL>Move</TL></Item>
<Item Id="archive" OnClick="OnContextMenuClick"><TL>Archive</TL></Item>
<Item Id="unarchive" OnClick="OnContextMenuClick"><TL>Unarchive</TL></Item>
<Item Id="download" OnClick="OnContextMenuClick"><TL>Download</TL></Item>
<Item Id="delete" OnClick="OnContextMenuClick"><TL>Delete</TL></Item>
</ContextMenu>
@code
{
[Parameter]
public IFileAccess FileAccess { get; set; }
// Data
private List<FileManagerObject> SelectedFiles { get; set; } = new();
private List<FileManagerObject> Objects { get; set; } = new();
private string CurrentPath = "";
// Search
private string SearchValue = "";
private string Search
{
get { return SearchValue; }
set
{
SearchValue = value;
InvokeAsync(StateHasChanged);
}
}
// States
private bool Loading = false;
// States - Editor
private FileManagerObject? Editing = null;
private string InitialEditorData = "";
private string Language = "plaintext";
// States - File Upload
private bool Uploading = false;
private int Percent = 0;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await RefreshActive();
}
}
private async Task RefreshActive()
{
Loading = true;
await InvokeAsync(StateHasChanged);
await Refresh(false);
Loading = false;
await InvokeAsync(StateHasChanged);
}
private async Task Refresh(bool rerender = true)
{
SelectedFiles.Clear();
Objects.Clear();
CurrentPath = await FileAccess.GetCurrentPath();
var data = await FileAccess.GetDirectoryContent();
Objects = data.ToList();
if (rerender)
await InvokeAsync(StateHasChanged);
}
private async Task CdPath(string path)
{
await FileAccess.ChangeDirectory(path);
await RefreshActive();
}
private async Task SetPath(string path)
{
await FileAccess.SetDirectory(path);
await RefreshActive();
}
private async Task OnContextMenuClick(ItemClickEventArgs e)
{
var data = e.Data as FileManagerObject;
if (data == null)
return;
switch (e.MenuItem.Id)
{
case "delete":
await FileAccess.Delete(data);
break;
case "download":
if (data.IsFile)
{
try
{
var stream = await FileAccess.GetDownloadStream(data);
await ToastService.Info(TranslationService.Translate("Starting download"));
await FileService.AddBuffer(stream);
await FileService.DownloadBinaryBuffers(data.Name);
}
catch (NotImplementedException)
{
try
{
var url = await FileAccess.GetDownloadUrl(data);
NavigationManager.NavigateTo(url, true);
await ToastService.Info(TranslationService.Translate("Starting download"));
}
catch (Exception exception)
{
await ToastService.Error(TranslationService.Translate("Error starting download"));
Logger.Error("Error downloading file");
Logger.Error(exception.Message);
}
}
catch (Exception exception)
{
await ToastService.Error(TranslationService.Translate("Error starting download"));
Logger.Error("Error downloading file stream");
Logger.Error(exception.Message);
}
}
break;
case "rename":
var newName = await AlertService.Text(TranslationService.Translate("Rename"), TranslationService.Translate("Enter a new name"), data.Name);
var path = await FileAccess.GetCurrentPath();
await FileAccess.Move(data, path + "/" + newName);
break;
}
await Refresh(false);
}
private async Task OnFileToggle(ChangeEventArgs obj, FileManagerObject o)
{
if ((bool)obj.Value)
{
if (SelectedFiles.Contains(o))
return;
SelectedFiles.Add(o);
await InvokeAsync(StateHasChanged);
}
else
{
if (!SelectedFiles.Contains(o))
return;
SelectedFiles.Remove(o);
await InvokeAsync(StateHasChanged);
}
}
private async Task OnAllFileToggle(ChangeEventArgs obj)
{
if ((bool)obj.Value)
{
foreach (var o in Objects)
{
if (SelectedFiles.Contains(o))
continue;
SelectedFiles.Add(o);
}
await InvokeAsync(StateHasChanged);
}
else
{
foreach (var o in Objects)
{
if (!SelectedFiles.Contains(o))
continue;
SelectedFiles.Remove(o);
}
await InvokeAsync(StateHasChanged);
}
}
private async Task CreateFolder()
{
var name = await AlertService.Text(TranslationService.Translate("Create a new folder"), TranslationService.Translate("Enter a name"), "");
if (string.IsNullOrEmpty(name))
return;
await FileAccess.CreateDirectory(name);
await Refresh();
}
private async void SaveFile(string data)
{
if (Editing == null)
return;
await FileAccess.WriteFile(Editing, data);
Editing = null;
await Refresh();
}
private async Task OpenFile(FileManagerObject o)
{
Editing = o;
Loading = true;
await InvokeAsync(StateHasChanged);
InitialEditorData = await FileAccess.ReadFile(Editing);
Language = MonacoTypeHelper.GetEditorType(Editing.Name);
Loading = false;
await InvokeAsync(StateHasChanged);
}
private async void CloseFile()
{
Editing = null;
await InvokeAsync(StateHasChanged);
}
private async Task Launch()
{
NavigationManager.NavigateTo(await FileAccess.GetLaunchUrl());
}
private async Task OnFileChanged(InputFileChangeEventArgs arg)
{
Uploading = true;
await InvokeAsync(StateHasChanged);
foreach (var browserFile in arg.GetMultipleFiles())
{
if (browserFile.Size < 1024 * 1024 * 100)
{
Percent = 0;
try
{
await FileAccess.UploadFile(
browserFile.Name,
browserFile.OpenReadStream(1024 * 1024 * 100),
async (i) =>
{
Percent = i;
Task.Run(() => { InvokeAsync(StateHasChanged); });
});
await Refresh();
}
catch (Exception e)
{
await ToastService.Error(TranslationService.Translate("An unknown error occured while uploading a file"));
Logger.Error("Error uploading file");
Logger.Error(e);
}
}
else
{
await ToastService.Error(TranslationService.Translate("The uploaded file should not be bigger than 100MB"));
}
}
Uploading = false;
await InvokeAsync(StateHasChanged);
await ToastService.Success(TranslationService.Translate("File upload complete"));
}
}

View file

@ -0,0 +1,47 @@
@using Moonlight.App.Helpers.Files
<div class="badge badge-lg badge-light-primary">
<div class="d-flex align-items-center flex-wrap">
@{
var vx = "/";
}
<a @onclick:preventDefault @onclick="() => SetPath(vx)" href="#">/</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
@{
var cp = "/";
var lp = "/";
var pathParts = Access.CurrentPath.Replace("\\", "/").Split('/', StringSplitOptions.RemoveEmptyEntries);
foreach (var path in pathParts)
{
lp = cp;
<a @onclick:preventDefault @onclick="() => SetPath(lp)" href="#">@(path)</a>
<span class="svg-icon svg-icon-2x svg-icon-primary mx-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6343 12.5657L8.45001 16.75C8.0358 17.1642 8.0358 17.8358 8.45001 18.25C8.86423 18.6642 9.5358 18.6642 9.95001 18.25L15.4929 12.7071C15.8834 12.3166 15.8834 11.6834 15.4929 11.2929L9.95001 5.75C9.5358 5.33579 8.86423 5.33579 8.45001 5.75C8.0358 6.16421 8.0358 6.83579 8.45001 7.25L12.6343 11.4343C12.9467 11.7467 12.9467 12.2533 12.6343 12.5657Z" fill="currentColor"></path>
</svg>
</span>
cp += path + "/";
}
}
</div>
</div>
@code
{
[Parameter]
public FileAccess Access { get; set; }
[Parameter]
public Func<Task>? OnPathChanged { get; set; }
public async Task SetPath(string path)
{
await Access.SetDir(path);
OnPathChanged?.Invoke();
}
}

View file

@ -0,0 +1,132 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject ModalService ModalService
@inject SmartTranslateService SmartTranslateService
<div class="modal" id="fileView@(Id)" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
@(Title)
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<FileView @ref="FileView"
Access="Access"
HideSelect="true"
Filter="DoFilter"
OnElementClicked="OnElementClicked">
</FileView>
</div>
<div class="modal-footer">
<WButton Text="@(SmartTranslateService.Translate("Submit"))"
WorkingText="@(SmartTranslateService.Translate("Processing"))"
CssClasses="btn-primary"
OnClick="Submit">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Cancel"))"
WorkingText="@(SmartTranslateService.Translate("Processing"))"
CssClasses="btn-danger"
OnClick="Cancel">
</WButton>
</div>
</div>
</div>
</div>
@code
{
[Parameter]
public FileAccess Access { get; set; }
[Parameter]
public bool OnlyFolder { get; set; } = false;
[Parameter]
public Func<FileData, bool>? Filter { get; set; }
[Parameter]
public string Title { get; set; } = "Select file or folder";
[Parameter]
public Func<string, Task>? OnSubmit { get; set; }
[Parameter]
public Func<Task>? OnCancel { get; set; }
private int Id = 0;
private string Result = "/";
private FileView FileView;
protected override void OnInitialized()
{
Id = this.GetHashCode();
}
public async Task Show()
{
// Reset
Result = "/";
await Access.SetDir("/");
await FileView.Refresh();
await ModalService.Show("fileView" + Id);
}
public async Task Hide()
{
await Cancel();
}
private async Task Cancel()
{
await ModalService.Hide("fileView" + Id);
await OnCancel?.Invoke()!;
}
private async Task Submit()
{
await ModalService.Hide("fileView" + Id);
await OnSubmit?.Invoke(Result)!;
}
private bool DoFilter(FileData file)
{
if (OnlyFolder)
{
if (file.IsFile)
return false;
else
{
if (Filter != null)
return Filter.Invoke(file);
else
return true;
}
}
else
{
if (Filter != null)
return Filter.Invoke(file);
else
return true;
}
}
private async Task<bool> OnElementClicked(FileData file)
{
Result = Access.CurrentPath + file.Name + (file.IsFile ? "" : "/");
if (!OnlyFolder && file.IsFile)
{
await Submit();
}
return false;
}
}

View file

@ -0,0 +1,90 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using Logging.Net
@inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden="" multiple=""/>
<label for="fileUpload" class="btn btn-primary me-3 @(Uploading ? "disabled" : "")">
@if (Uploading)
{
<span>@(Percent)%</span>
}
else
{
<span class="svg-icon svg-icon-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M10 4H21C21.6 4 22 4.4 22 5V7H10V4Z" fill="currentColor"></path>
<path d="M10.4 3.60001L12 6H21C21.6 6 22 6.4 22 7V19C22 19.6 21.6 20 21 20H3C2.4 20 2 19.6 2 19V4C2 3.4 2.4 3 3 3H9.20001C9.70001 3 10.2 3.20001 10.4 3.60001ZM16 11.6L12.7 8.29999C12.3 7.89999 11.7 7.89999 11.3 8.29999L8 11.6H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H16Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 11.6V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V11.6H11Z" fill="currentColor"></path>
</svg>
</span>
<TL>Upload</TL>
}
</label>
@code
{
[Parameter]
public FileAccess Access { get; set; }
[Parameter]
public Func<Task> OnUploadComplete { get; set; }
private bool Uploading = false;
private int Percent = 0;
private async Task OnFileChanged(InputFileChangeEventArgs arg)
{
await ToastService.CreateProcessToast("upload", SmartTranslateService.Translate("Uploading files"));
Uploading = true;
await InvokeAsync(StateHasChanged);
int i = 1;
foreach (var browserFile in arg.GetMultipleFiles())
{
if (browserFile.Size < 1024 * 1024 * 100)
{
Percent = 0;
try
{
await Access.Upload(
browserFile.Name,
browserFile.OpenReadStream(1024 * 1024 * 100),
async (i) =>
{
Percent = i;
Task.Run(() => { InvokeAsync(StateHasChanged); });
});
OnUploadComplete?.Invoke();
}
catch (Exception e)
{
await ToastService.Error(SmartTranslateService.Translate("An unknown error occured while uploading a file"));
Logger.Error("Error uploading file");
Logger.Error(e);
}
await ToastService.UpdateProcessToast("upload", $"{i}/{arg.GetMultipleFiles().Count} {SmartTranslateService.Translate("complete")}");
}
else
{
await ToastService.Error(SmartTranslateService.Translate("The uploaded file should not be bigger than 100MB"));
}
i++;
}
Uploading = false;
await InvokeAsync(StateHasChanged);
await ToastService.UpdateProcessToast("upload", SmartTranslateService.Translate("Upload complete"));
await ToastService.RemoveProcessToast("upload");
}
}

View file

@ -0,0 +1,276 @@
@using Moonlight.App.Helpers.Files
@using Logging.Net
@using BlazorContextMenu
@using Moonlight.App.Helpers
<div class="table-responsive">
<div class="dataTables_scroll">
<div class="dataTables_scrollHead">
<div class="dataTables_scrollHeadInner">
<table class="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer">
<thead>
<tr class="text-start text-gray-400 fw-bold fs-7 text-uppercase gs-0">
<th class="w-10px pe-2 sorting_disabled">
@if (!HideSelect)
{
<div class="form-check form-check-sm form-check-custom form-check-solid me-3">
@if (AllToggled)
{
<input @onclick="() => SetToggleState(false)" class="form-check-input" type="checkbox" checked="">
}
else
{
<input @onclick="() => SetToggleState(true)" class="form-check-input" type="checkbox">
}
</div>
}
</th>
<th class="min-w-250px sorting_disabled">Name</th>
</tr>
</thead>
</table>
</div>
</div>
<div class="dataTables_scrollBody" style="@(DisableScrolling ? "" : "position: relative; overflow: auto; max-height: 700px; width: 100%;")">
<table class="table align-middle table-row-dashed fs-6 gy-5 dataTable no-footer" style="width: 100%;">
<tbody class="fw-semibold text-gray-600">
<LazyLoader Load="Load">
<tr class="even">
<td class="w-10px">
</td>
<td>
<div class="d-flex align-items-center">
<span class="icon-wrapper">
<i class="bx bx-md bx-up-arrow-alt text-primary"></i>
</span>
<a href="#" @onclick:preventDefault @onclick="GoUp" class="ms-3 text-gray-800 text-hover-primary">
<TL>Go up</TL>
</a>
</div>
</td>
<td></td>
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2">
</div>
</div>
</td>
</tr>
@foreach (var file in Data)
{
<tr class="even">
<td class="w-10px">
@if (!HideSelect)
{
<div class="form-check form-check-sm form-check-custom form-check-solid">
@{
var toggle = ToggleStatusCache.ContainsKey(file) && ToggleStatusCache[file];
}
@if (toggle)
{
<input @onclick="() => SetToggleState(file, false)" class="form-check-input" type="checkbox" checked="checked">
}
else
{
<input @onclick="() => SetToggleState(file, true)" class="form-check-input" type="checkbox">
}
</div>
}
</td>
<td>
<div class="d-flex align-items-center">
<span class="icon-wrapper">
@if (file.IsFile)
{
<i class="bx bx-md bx-file text-primary"></i>
}
else
{
<i class="bx bx-md bx-folder text-primary"></i>
}
</span>
<a href="#" @onclick:preventDefault @onclick="() => OnClicked(file)" class="ms-3 text-gray-800 text-hover-primary">@(file.Name)</a>
</div>
</td>
<td>@(Formatter.FormatSize(file.Size))</td>
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2 me-7">
@if (ContextActions.Any())
{
<ContextMenuTrigger MenuId="triggerMenu" MouseButtonTrigger="MouseButtonTrigger.Both" Data="file">
<button class="btn btn-sm btn-icon btn-light btn-active-light-primary me-2">
<span class="svg-icon svg-icon-5 m-0">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="17" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
<rect x="3" y="10" width="4" height="4" rx="2" fill="currentColor"></rect>
</svg>
</span>
</button>
</ContextMenuTrigger>
}
</div>
</div>
</td>
</tr>
}
</LazyLoader>
</tbody>
</table>
</div>
</div>
</div>
@if (ContextActions.Any())
{
<ContextMenu Id="triggerMenu" CssClass="bg-secondary z-10">
@foreach (var action in ContextActions)
{
<Item Id="@action.Id" OnClick="OnContextMenuClick">
<TL>@action.Name</TL>
</Item>
}
</ContextMenu>
}
@code
{
[Parameter]
public FileAccess Access { get; set; }
[Parameter]
public Func<FileData, Task<bool>>? OnElementClicked { get; set; }
[Parameter]
public Func<Task>? OnSelectionChanged { get; set; }
[Parameter]
public ContextAction[] ContextActions { get; set; } = Array.Empty<ContextAction>();
[Parameter]
public bool HideSelect { get; set; } = false;
[Parameter]
public bool DisableScrolling { get; set; } = false;
[Parameter]
public Func<FileData, bool>? Filter { get; set; }
public FileData[] SelectedFiles => ToggleStatusCache
.Where(x => x.Value)
.Select(x => x.Key)
.ToArray();
private FileData[] Data = Array.Empty<FileData>();
private Dictionary<FileData, bool> ToggleStatusCache = new();
private bool AllToggled = false;
public async Task Refresh()
{
var list = new List<FileData>();
foreach (var fileData in await Access.Ls())
{
if (Filter != null)
{
if(Filter.Invoke(fileData))
list.Add(fileData);
}
else
list.Add(fileData);
}
Data = list.ToArray();
ToggleStatusCache.Clear();
AllToggled = false;
foreach (var fileData in Data)
{
ToggleStatusCache.Add(fileData, false);
}
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task Load(LazyLoader arg)
{
await Refresh();
}
private async Task SetToggleState(FileData fileData, bool status)
{
if (ToggleStatusCache.ContainsKey(fileData))
ToggleStatusCache[fileData] = status;
else
ToggleStatusCache.Add(fileData, status);
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task SetToggleState(bool status)
{
AllToggled = status;
foreach (var fd in ToggleStatusCache.Keys)
{
ToggleStatusCache[fd] = status;
}
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task OnClicked(FileData fileData)
{
if (OnElementClicked != null)
{
var canceled = await OnElementClicked.Invoke(fileData);
if (canceled)
return;
}
if (!fileData.IsFile)
{
await Access.Cd(fileData.Name);
await Refresh();
}
}
private async Task GoUp()
{
if (OnElementClicked != null)
{
var canceled = await OnElementClicked.Invoke(new()
{
Name = "..",
IsFile = false
});
if (canceled)
return;
}
await Access.Up();
await Refresh();
}
private Task OnContextMenuClick(ItemClickEventArgs eventArgs)
{
var action = ContextActions.FirstOrDefault(x => x.Id == eventArgs.MenuItem.Id);
if (action != null)
{
action.Action.Invoke((FileData)eventArgs.Data);
}
return Task.CompletedTask;
}
}

View file

@ -1,7 +1,7 @@
<div class="alert alert-primary d-flex rounded p-6">
<div class="d-flex flex-stack flex-grow-1 flex-wrap flex-md-nowrap">
<div class="mb-3 mb-md-0 fw-semibold">
<h4 class="text-gray-900 fw-bold"><TL>Plugins</TL></h4>
<h4 class="text-gray-900 fw-bold"><TL>Addons</TL></h4>
<div class="fs-6 text-gray-700 pe-7">
<TL>This feature is currently not available</TL>
</div>

View file

@ -15,21 +15,20 @@
@inject AlertService AlertService
@inject SmartTranslateService TranslationService
<div class="row g-5 g-xl-10 mb-xl-10">
<Terminal @ref="Terminal" RunOnFirstRender="RunOnFirstRender"></Terminal>
<div class="mt-3 row">
<div class="ms-2 input-group">
<div class="card card-body rounded bg-black p-3">
<div class="d-flex flex-column">
<Terminal @ref="Terminal" RunOnFirstRender="RunOnFirstRender"></Terminal>
<div class="input-group">
<script suppress-error="BL9992">
function checkEnter(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById("sendCmd").click();
}
}
</script>
<input @bind="@CommandInput" class="form-control" onkeyup="checkEnter(event)" placeholder="@(TranslationService.Translate("Enter command"))"/>
function checkEnter(event)
{
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById("sendCmd").click();
}
}
</script>
<input @bind="@CommandInput" class="form-control rounded-start" onkeyup="checkEnter(event)" placeholder="@(TranslationService.Translate("Enter command"))"/>
<button id="sendCmd" @onclick="SendCommand" class="input-group-text btn btn-primary">@(TranslationService.Translate("Execute"))</button>
</div>
</div>

View file

@ -1,27 +1,27 @@
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Services
@using Moonlight.App.Helpers
@using Moonlight.App.Models.Files
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@inject ServerService ServerService
@inject IdentityService IdentityService
@inject WingsApiHelper WingsApiHelper
@inject WingsJwtHelper WingsJwtHelper
@inject ConfigService ConfigService
<LazyLoader Load="Load">
<FileManager FileAccess="FileAccess"></FileManager>
</LazyLoader>
<FileManager Access="FileAccess"></FileManager>
@code
{
[CascadingParameter]
public Server CurrentServer { get; set; }
[CascadingParameter]
public User User { get; set; }
private IFileAccess FileAccess;
private FileAccess FileAccess;
private async Task Load(LazyLoader arg)
protected override void OnInitialized()
{
var user = await IdentityService.Get(); // User for launch url
FileAccess = await ServerService.CreateFileAccess(CurrentServer, user);
FileAccess = new WingsFileAccess(WingsApiHelper, WingsJwtHelper, CurrentServer, ConfigService, User);
}
}

View file

@ -134,10 +134,10 @@
</a>
</li>
<li class="nav-item w-100 me-0 mb-md-2">
<a href="/server/@(CurrentServer.Uuid)/plugins" class="nav-link w-100 btn btn-flex @(Index == 4 ? "active" : "") btn-active-light-primary">
<a href="/server/@(CurrentServer.Uuid)/addons" class="nav-link w-100 btn btn-flex @(Index == 4 ? "active" : "") btn-active-light-primary">
<i class="bx bx-plug bx-sm me-2"></i>
<span class="d-flex flex-column align-items-start">
<span class="fs-5"><TL>Plugins</TL></span>
<span class="fs-5"><TL>Addons</TL></span>
</span>
</a>
</li>

View file

@ -1,51 +1,64 @@
@using PteroConsole.NET
@using Moonlight.App.Database.Entities
@using Moonlight.Shared.Components.ServerControl.Settings
@using Microsoft.AspNetCore.Components.Rendering
<div class="row mb-5">
@if (Tags.Contains("paperversion"))
{
<PaperVersionSetting></PaperVersionSetting>
}
@if (Tags.Contains("pythonversion"))
{
<PythonVersionSetting></PythonVersionSetting>
}
@{
/*
* @if (Tags.Contains("pythonfile"))
{
<PythonFileSetting></PythonFileSetting>
}
@if (Tags.Contains("javascriptfile"))
{
<JavascriptFileSetting></JavascriptFileSetting>
}
*/
}
@if (Tags.Contains("javascriptversion"))
{
<JavascriptVersionSetting></JavascriptVersionSetting>
}
@if (Tags.Contains("join2start"))
{
<Join2StartSetting></Join2StartSetting>
}
</div>
<LazyLoader Load="Load">
<div class="accordion" id="serverSetting">
@foreach (var setting in Settings)
{
<div class="accordion-item">
<h2 class="accordion-header" id="serverSetting-header@(setting.GetHashCode())">
<button class="accordion-button fs-4 fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#serverSetting-body@(setting.GetHashCode())" aria-expanded="true" aria-controls="serverSetting-body@(setting.GetHashCode())">
<TL>@(setting.Key)</TL>
</button>
</h2>
<div id="serverSetting-body@(setting.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="serverSetting-header@(setting.GetHashCode())" data-bs-parent="#serverSetting">
<div class="accordion-body">
@(GetComponent(setting.Value))
</div>
</div>
</div>
}
</div>
</LazyLoader>
@code
{
[CascadingParameter]
public PteroConsole Console { get; set; }
[CascadingParameter]
public Server CurrentServer { get; set; }
[CascadingParameter]
public string[] Tags { get; set; }
private Dictionary<string, Type> Settings = new();
private Task Load(LazyLoader lazyLoader)
{
if(Tags.Contains("paperversion"))
Settings.Add("Paper version", typeof(PaperVersionSetting));
if(Tags.Contains("join2start"))
Settings.Add("Join2Start", typeof(Join2StartSetting));
if(Tags.Contains("javascriptversion"))
Settings.Add("Javascript version", typeof(JavascriptVersionSetting));
if(Tags.Contains("javascriptfile"))
Settings.Add("Javascript file", typeof(JavascriptFileSetting));
if(Tags.Contains("pythonversion"))
Settings.Add("Python version", typeof(PythonVersionSetting));
if(Tags.Contains("pythonfile"))
Settings.Add("Python file", typeof(PythonFileSetting));
return Task.CompletedTask;
}
private RenderFragment GetComponent(Type type) => builder =>
{
builder.OpenComponent(0, type);
builder.CloseComponent();
};
}

View file

@ -0,0 +1,82 @@
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Repositories.Servers
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@inject ServerRepository ServerRepository
@inject WingsApiHelper WingsApiHelper
@inject SmartTranslateService SmartTranslateService
<div class="col">
<div class="card card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<label class="mb-2 form-label">
<TL>Javascript file</TL>
</label>
<input type="text" class="mb-2 form-control disabled" disabled="" value="@(PathAndFile)"/>
<button @onclick="Show" class="btn btn-primary"><TL>Change</TL></button>
</LazyLoader>
</div>
</div>
<FileSelectModal @ref="FileSelectModal"
Access="Access"
Filter="@(x => !x.IsFile || x.Name.EndsWith(".js"))"
Title="@(SmartTranslateService.Translate("Select javascript file to execute on start"))"
OnlyFolder="false"
OnCancel="() => { return Task.CompletedTask; }"
OnSubmit="OnSubmit">
</FileSelectModal>
@code
{
[CascadingParameter]
public Server CurrentServer { get; set; }
private string PathAndFile;
private FileAccess Access;
private FileSelectModal FileSelectModal;
private LazyLoader LazyLoader;
protected override void OnInitialized()
{
Access = new WingsFileAccess(WingsApiHelper,
null!,
CurrentServer,
null!,
null!
);
}
private async Task Load(LazyLoader lazyLoader)
{
var v = CurrentServer.Variables.FirstOrDefault(x => x.Key == "BOT_JS_FILE");
PathAndFile = v != null ? v.Value : "";
await InvokeAsync(StateHasChanged);
}
private async Task Show()
{
await FileSelectModal.Show();
}
private async Task OnSubmit(string path)
{
var v = CurrentServer.Variables.FirstOrDefault(x => x.Key == "BOT_JS_FILE");
if (v != null)
{
v.Value = path.TrimStart("/"[0]);
ServerRepository.Update(CurrentServer);
}
await LazyLoader.Reload();
}
}

View file

@ -8,29 +8,34 @@
@inject ServerRepository ServerRepository
@inject ImageRepository ImageRepository
@inject SmartTranslateService TranslationService
<div class="col">
<div class="card card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<label class="mb-2 form-label"><TL>Javascript Version</TL></label>
<select class="mb-2 form-select" @bind="Image">
@foreach (var image in Images)
<label class="mb-2 form-label"><TL>Javascript version</TL></label>
<select @bind="ImageIndex" class="form-select mb-2">
@foreach (var image in DockerImages)
{
if (image == Image)
if (image.Id == SelectedImage.Id)
{
<option value="@(image)" selected="">@(image)</option>
<option value="@(image.Id)" selected="selected">
@(ParseHelper.FirstPartStartingWithNumber(image.Name))
</option>
}
else
{
<option value="@(image)">@(image)</option>
<option value="@(image.Id)">
@(ParseHelper.FirstPartStartingWithNumber(image.Name))
</option>
}
}
</select>
<WButton
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary"></WButton>
<WButton
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary">
</WButton>
</LazyLoader>
</div>
</div>
@ -40,44 +45,36 @@
[CascadingParameter]
public Server CurrentServer { get; set; }
private string[] Images;
private string Image;
private LazyLoader LazyLoader;
private List<DockerImage> DockerImages;
private DockerImage SelectedImage;
private async Task Load(LazyLoader lazyLoader)
private int ImageIndex
{
//TODO: Check if this is a redundant call
var serverImage = ImageRepository
get => SelectedImage.Id;
set { SelectedImage = DockerImages.First(x => x.Id == value); }
}
private Task Load(LazyLoader lazyLoader)
{
var image = ImageRepository
.Get()
.Include(x => x.DockerImages)
.First(x => x.Id == CurrentServer.Image.Id);
Image = ParseHelper.FirstPartStartingWithNumber(serverImage.DockerImages.First(x => x.Id == CurrentServer.DockerImageIndex).Name);
var res = new List<string>();
foreach (var image in serverImage.DockerImages)
{
res.Add(ParseHelper.FirstPartStartingWithNumber(image.Name));
}
Images = res.ToArray();
await InvokeAsync(StateHasChanged);
DockerImages = image.DockerImages;
SelectedImage = DockerImages[CurrentServer.DockerImageIndex];
return Task.CompletedTask;
}
private async Task Save()
{
var serverImage = ImageRepository
.Get()
.Include(x => x.DockerImages)
.First(x => x.Id == CurrentServer.Image.Id);
var allImages = serverImage.DockerImages;
var imageToUse = allImages.First(x => x.Name.EndsWith(Image));
CurrentServer.DockerImageIndex = allImages.IndexOf(imageToUse);
ServerRepository.Update(CurrentServer);
CurrentServer.DockerImageIndex = DockerImages.IndexOf(SelectedImage);
ServerRepository.Update(CurrentServer);
await LazyLoader.Reload();
}
}

View file

@ -1,7 +1,5 @@
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Partials
@using Task = System.Threading.Tasks.Task
@using Logging.Net
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Entities
@using Moonlight.App.Repositories

View file

@ -0,0 +1,82 @@
@using Task = System.Threading.Tasks.Task
@using Moonlight.App.Repositories.Servers
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services
@inject ServerRepository ServerRepository
@inject WingsApiHelper WingsApiHelper
@inject SmartTranslateService SmartTranslateService
<div class="col">
<div class="card card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<label class="mb-2 form-label">
<TL>Python file</TL>
</label>
<input type="text" class="mb-2 form-control disabled" disabled="" value="@(PathAndFile)"/>
<button @onclick="Show" class="btn btn-primary"><TL>Change</TL></button>
</LazyLoader>
</div>
</div>
<FileSelectModal @ref="FileSelectModal"
Access="Access"
Filter="@(x => !x.IsFile || x.Name.EndsWith(".py"))"
Title="@(SmartTranslateService.Translate("Select python file to execute on start"))"
OnlyFolder="false"
OnCancel="() => { return Task.CompletedTask; }"
OnSubmit="OnSubmit">
</FileSelectModal>
@code
{
[CascadingParameter]
public Server CurrentServer { get; set; }
private string PathAndFile;
private FileAccess Access;
private FileSelectModal FileSelectModal;
private LazyLoader LazyLoader;
protected override void OnInitialized()
{
Access = new WingsFileAccess(WingsApiHelper,
null!,
CurrentServer,
null!,
null!
);
}
private async Task Load(LazyLoader lazyLoader)
{
var v = CurrentServer.Variables.FirstOrDefault(x => x.Key == "BOT_PY_FILE");
PathAndFile = v != null ? v.Value : "";
await InvokeAsync(StateHasChanged);
}
private async Task Show()
{
await FileSelectModal.Show();
}
private async Task OnSubmit(string path)
{
var v = CurrentServer.Variables.FirstOrDefault(x => x.Key == "BOT_PY_FILE");
if (v != null)
{
v.Value = path.TrimStart("/"[0]);
ServerRepository.Update(CurrentServer);
}
await LazyLoader.Reload();
}
}

View file

@ -1,6 +1,4 @@
@using Moonlight.App.Services
@using Task = System.Threading.Tasks.Task
@using Moonlight.Shared.Components.Partials
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Servers
@ -15,24 +13,29 @@
<div class="card card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<label class="mb-2 form-label"><TL>Python version</TL></label>
<select class="mb-2 form-select" @bind="Image">
@foreach (var image in Images)
<select @bind="ImageIndex" class="form-select mb-2">
@foreach (var image in DockerImages)
{
if (image == Image)
if (image.Id == SelectedImage.Id)
{
<option value="@(image)" selected="">@(image)</option>
<option value="@(image.Id)" selected="selected">
@(ParseHelper.FirstPartStartingWithNumber(image.Name))
</option>
}
else
{
<option value="@(image)">@(image)</option>
<option value="@(image.Id)">
@(ParseHelper.FirstPartStartingWithNumber(image.Name))
</option>
}
}
</select>
<WButton
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary"></WButton>
<WButton
OnClick="Save"
Text="@(TranslationService.Translate("Change"))"
WorkingText="@(TranslationService.Translate("Changing"))"
CssClasses="btn-primary">
</WButton>
</LazyLoader>
</div>
</div>
@ -42,43 +45,36 @@
[CascadingParameter]
public Server CurrentServer { get; set; }
private string[] Images;
private string Image;
private LazyLoader LazyLoader;
private List<DockerImage> DockerImages;
private DockerImage SelectedImage;
private async Task Load(LazyLoader lazyLoader)
private int ImageIndex
{
var serverImage = ImageRepository
get => SelectedImage.Id;
set { SelectedImage = DockerImages.First(x => x.Id == value); }
}
private Task Load(LazyLoader lazyLoader)
{
var image = ImageRepository
.Get()
.Include(x => x.DockerImages)
.First(x => x.Id == CurrentServer.Image.Id);
Image = ParseHelper.FirstPartStartingWithNumber(serverImage.DockerImages.First(x => x.Id == CurrentServer.DockerImageIndex).Name);
var res = new List<string>();
foreach (var image in serverImage.DockerImages)
{
res.Add(ParseHelper.FirstPartStartingWithNumber(image.Name));
}
Images = res.ToArray();
await InvokeAsync(StateHasChanged);
DockerImages = image.DockerImages;
SelectedImage = DockerImages[CurrentServer.DockerImageIndex];
return Task.CompletedTask;
}
private async Task Save()
{
var serverImage = ImageRepository
.Get()
.Include(x => x.DockerImages)
.First(x => x.Id == CurrentServer.Image.Id);
var allImages = serverImage.DockerImages;
var imageToUse = allImages.First(x => x.Name.EndsWith(Image));
CurrentServer.DockerImageIndex = allImages.IndexOf(imageToUse);
ServerRepository.Update(CurrentServer);
CurrentServer.DockerImageIndex = DockerImages.IndexOf(SelectedImage);
ServerRepository.Update(CurrentServer);
await LazyLoader.Reload();
}
}

View file

@ -8,7 +8,7 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Xterm
@using Moonlight.Shared.Components.ServerControl
@using Newtonsoft.Json
@ -16,13 +16,24 @@
@inject ImageRepository ImageRepository
@inject ServerRepository ServerRepository
@inject WingsConsoleHelper WingsConsoleHelper
@inject IdentityService IdentityService
@inject MessageService MessageService
@inject NavigationManager NavigationManager
@implements IDisposable
<LazyLoader Load="LoadData">
@if (CurrentServer == null)
{
<div class="alert alert-danger">
<TL>Server not found</TL>
<div class="d-flex justify-content-center flex-center">
<div class="card">
<img src="/assets/media/svg/nodata.svg" class="card-img-top w-25 mx-auto pt-5" alt="Not found image" />
<div class="card-body text-center">
<h4 class="card-title"><TL>Server not found</TL></h4>
<p class="card-text">
<TL>A server with that id cannot be found or you have no access for this server</TL>
</p>
</div>
</div>
</div>
}
else
@ -30,6 +41,21 @@
if (Console.ConnectionState == ConnectionState.Connected)
{
if (Console.ServerState == ServerState.Installing)
{
<div class="card">
<div class="card-body">
<div class="mb-10">
<div class="fs-2hx fw-bold text-gray-800 text-center mb-13">
<span class="me-2">
<TL>Server installation is currently running</TL>
</span>
</div>
</div>
<Terminal @ref="InstallConsole"></Terminal>
</div>
</div>
}
else if (CurrentServer.Installing)
{
<div class="card">
<div class="card-body">
@ -66,7 +92,7 @@
case "network":
index = 3;
break;
case "plugins":
case "addons":
index = 4;
break;
case "settings":
@ -90,8 +116,8 @@
case "network":
<ServerNetwork></ServerNetwork>
break;
case "plugins":
<ServerPlugins></ServerPlugins>
case "addons":
<ServerAddons></ServerAddons>
break;
case "settings":
<ServerSettings></ServerSettings>
@ -179,7 +205,7 @@
.Include(x => x.Owner)
.First(x => x.Uuid == uuid);
if (CurrentServer.Owner.Id != User!.Id)
if (CurrentServer.Owner.Id != User!.Id && User.Admin)
CurrentServer = null;
}
catch (Exception)
@ -201,10 +227,28 @@
await lazyLoader.SetText("Connecting to console");
await WingsConsoleHelper.ConnectWings(Console!, CurrentServer);
MessageService.Subscribe<Index, Server>($"server.{CurrentServer.Uuid}.installcomplete", this, server =>
{
Task.Run(() =>
{
NavigationManager.NavigateTo(NavigationManager.Uri);
});
return Task.CompletedTask;
});
}
else
{
Logger.Debug("Server is null");
}
}
public void Dispose()
{
if (CurrentServer != null)
{
MessageService.Unsubscribe($"server.{CurrentServer.Uuid}.installcomplete", this);
}
}
}

View file

@ -1,55 +1,39 @@
@page "/test"
@using Moonlight.App.Services.Interop
@using Moonlight.App.Database.Entities
@using Moonlight.App.Repositories.Domains
@inject ToastService ToastService
@inject SharedDomainRepository SharedDomainRepository
@using Moonlight.Shared.Components.FileManagerPartials
@using Moonlight.App.Repositories.Servers
@using Moonlight.App.Helpers.Files
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Helpers
@using Moonlight.App.Services
@using User = Moonlight.App.Database.Entities.User
@inject ServerRepository ServerRepository
@inject WingsApiHelper WingsApiHelper
@inject WingsJwtHelper WingsJwtHelper
@inject ConfigService ConfigService
<LazyLoader Load="Load">
<SmartForm Model="Domain" OnValidSubmit="Callback">
<div class="mb-3">
<label class="form-label">
<TL>Domain name</TL>
</label>
<InputText @bind-Value="Domain.Name" class="form-control"></InputText>
</div>
<div class="mb-3">
<label class="form-label">
<TL>Shared domain</TL>
</label>
<SmartSelect TField="SharedDomain"
@bind-Value="Domain.SharedDomain"
Items="SharedDomains"
DisplayField="@(x => x.Name)">
</SmartSelect>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</SmartForm>
<FileManager Access="FileAccess">
</FileManager>
</LazyLoader>
@code
{
private App.Database.Entities.Domain Domain = new();
private SharedDomain[] SharedDomains;
private async Task Callback(EditContext obj)
{
Console.WriteLine(Domain.Name);
Console.WriteLine(Domain.SharedDomain.Name);
await ToastService.Success("SUCCESS");
}
[CascadingParameter]
public User User { get; set; }
private FileAccess FileAccess;
private Task Load(LazyLoader arg)
{
SharedDomains = SharedDomainRepository
var server = ServerRepository
.Get()
.ToArray();
.Include(x => x.Node)
.First();
FileAccess = new WingsFileAccess(WingsApiHelper, WingsJwtHelper, server, ConfigService, User);
return Task.CompletedTask;
}
}

View file

@ -406,26 +406,4 @@ The City field is required.;The City field is required.
The State field is required.;The State field is required.
The Country field is required.;The Country field is required.
Street and house number requered;Street and house number requered
Max lenght reached;Max lenght reached
stopped;stopped
Cleanups;Cleanups
executed;executed
Used clanup;Used clanup
Checking Nodes;Checking Nodes
Enable;Enable
Disabble;Disabble
Checking found servers;Checking found servers
Scanning servers;Scanning servers
Scanning code containers;Scanning code containers
Cleanup finifhed. Duration: 0.08 Minuten;Cleanup finifhed. Duration: 0.08 Minuten
Disable;Disable
Cleanup finished. Duration: 0.08 Minuten;Cleanup finished. Duration: 0.08 Minuten
Cleanup finished. Duration: 0.08 Minutes;Cleanup finished. Duration: 0.08 Minutes
Cleanup finished. Duration: 0.13 Minutes;Cleanup finished. Duration: 0.13 Minutes
Cleanup finished. Duration: 0.23 Minutes;Cleanup finished. Duration: 0.23 Minutes
Cleanup finished. Duration: 0.14 Minutes;Cleanup finished. Duration: 0.14 Minutes
Cleanup finished. Duration: 0.09 Minutes;Cleanup finished. Duration: 0.09 Minutes
Cleanup finished. Duration: 0.1 Minutes;Cleanup finished. Duration: 0.1 Minutes
Cleanup finished. Duration: 0.11 Minutes;Cleanup finished. Duration: 0.11 Minutes
Cleanup Exceptions;Cleanup Exceptions
Server not found;Server not found
Max lenght reached;Max lenght reached

View file

@ -0,0 +1,12 @@
window.initMonacoTheme = function ()
{
monaco.editor.defineTheme('moonlight-theme', {
base: 'vs-dark',
inherit: true,
rules: [
],
colors: {
'editor.background': '#000000'
}
});
}

View file

@ -0,0 +1,13 @@
window.moonlight =
{
modals: {
show: function (name)
{
$('#' + name).modal('show');
},
hide: function (name)
{
$('#' + name).modal('hide');
}
}
};

View file

@ -1,19 +1,52 @@
window.showInfoToast = function (msg)
{
window.showInfoToast = function (msg) {
toastr['info'](msg);
}
window.showErrorToast = function (msg)
{
window.showErrorToast = function (msg) {
toastr['error'](msg);
}
window.showSuccessToast = function (msg)
{
window.showSuccessToast = function (msg) {
toastr['success'](msg);
}
window.showWarningToast = function (msg)
{
window.showWarningToast = function (msg) {
toastr['warning'](msg);
}
window.createToast = function (id, text) {
var toast = toastr.success(text, '',
{
closeButton: true,
progressBar: false,
tapToDismiss: false,
timeOut: 0,
extendedTimeOut: 0,
positionClass: "toastr-bottom-right",
preventDuplicates: false,
onclick: function () {
toastr.clear(toast);
}
});
var toastElement = toast[0];
toastElement.setAttribute('data-toast-id', id);
toastElement.classList.add("bg-secondary");
}
window.modifyToast = function (id, newText) {
var toast = document.querySelector('[data-toast-id="' + id + '"]');
if (toast) {
var toastMessage = toast.lastChild;
if (toastMessage) {
toastMessage.innerHTML = newText;
}
}
}
window.removeToast = function (id) {
var toast = document.querySelector('[data-toast-id="' + id + '"]');
if (toast) {
toast.childNodes.item(1).click();
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="647.63626" height="632.17383" viewBox="0 0 647.63626 632.17383" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)" fill="#f2f2f2"/><path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)" fill="#6c63ff"/><circle cx="190.15351" cy="24.95465" r="20" fill="#6c63ff"/><circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/><path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)" fill="#e6e6e6"/><path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)" fill="#6c63ff"/><circle cx="433.63626" cy="105.17383" r="20" fill="#6c63ff"/><circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB