New file manager complete. Server settings for py, js, mc complete. Fixes

This commit is contained in:
Marcel Baumgartner
2023-04-03 00:09:03 +02:00
parent 02f6386f95
commit 6b8c75d014
39 changed files with 1332 additions and 356 deletions

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

@@ -1,19 +0,0 @@
namespace Moonlight.App.Helpers.Files;
public interface IFileAccess
{
public Task<FileData[]> Ls();
public Task Cd(string dir);
public Task Up();
public Task SetDir(string dir);
public Task<string> Read(FileData fileData);
public Task Write(FileData fileData, string content);
public Task Upload(string name, Stream stream, Action<int>? progressUpdated = null);
public Task MkDir(string name);
public Task<string> Pwd();
public Task<string> DownloadUrl(FileData fileData);
public Task<Stream> DownloadStream(FileData fileData);
public Task Delete(FileData fileData);
public Task Move(FileData fileData, string newPath);
public Task<string> GetLaunchUrl();
}

View File

@@ -1,21 +1,32 @@
using Moonlight.App.Database.Entities;
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 : IFileAccess
public class WingsFileAccess : FileAccess
{
private readonly WingsApiHelper WingsApiHelper;
private readonly WingsJwtHelper WingsJwtHelper;
private readonly ConfigService ConfigService;
private readonly Server Server;
private readonly User User;
private string CurrentPath = "/";
public WingsFileAccess(WingsApiHelper wingsApiHelper, WingsJwtHelper wingsJwtHelper,Server server)
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)
{
@@ -23,9 +34,9 @@ public class WingsFileAccess : IFileAccess
}
}
public async Task<FileData[]> Ls()
public override async Task<FileData[]> Ls()
{
var res = await WingsApiHelper.Get<ListDirectoryRequest[]>(
var res = await WingsApiHelper.Get<ListDirectory[]>(
Server.Node,
$"api/servers/{Server.Uuid}/files/list-directory?directory={CurrentPath}"
);
@@ -45,7 +56,7 @@ public class WingsFileAccess : IFileAccess
return x.ToArray();
}
public Task Cd(string dir)
public override Task Cd(string dir)
{
var x = Path.Combine(CurrentPath, dir).Replace("\\", "/") + "/";
x = x.Replace("//", "/");
@@ -54,65 +65,168 @@ public class WingsFileAccess : IFileAccess
return Task.CompletedTask;
}
public Task Up()
public override Task Up()
{
CurrentPath = Path.GetFullPath(Path.Combine(CurrentPath, "..")).Replace("\\", "/").Replace("C:", "");
return Task.CompletedTask;
}
public Task SetDir(string dir)
public override Task SetDir(string dir)
{
CurrentPath = dir;
return Task.CompletedTask;
}
public async Task<string> Read(FileData fileData)
public override async Task<string> Read(FileData fileData)
{
return await WingsApiHelper.GetRaw(Server.Node,$"api/servers/{Server.Uuid}/files/contents?file={CurrentPath}{fileData.Name}");
return await WingsApiHelper.GetRaw(Server.Node,
$"api/servers/{Server.Uuid}/files/contents?file={CurrentPath}{fileData.Name}");
}
public async Task Write(FileData fileData, string content)
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);
await WingsApiHelper.PostRaw(Server.Node,
$"api/servers/{Server.Uuid}/files/write?file={CurrentPath}{fileData.Name}", content);
}
public Task Upload(string name, Stream stream, Action<int>? progressUpdated = null)
public override async Task Upload(string name, Stream dataStream, Action<int>? progressUpdated = null)
{
throw new NotImplementedException();
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 Task MkDir(string name)
public override async Task MkDir(string name)
{
throw new NotImplementedException();
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/create-directory",
new CreateDirectory()
{
Name = name,
Path = CurrentPath
}
);
}
public Task<string> Pwd()
public override Task<string> Pwd()
{
return Task.FromResult(CurrentPath);
}
public Task<string> DownloadUrl(FileData fileData)
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 Task<Stream> DownloadStream(FileData fileData)
public override async Task Delete(FileData fileData)
{
throw new NotImplementedException();
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/delete", new DeleteFiles()
{
Root = CurrentPath,
Files = new()
{
fileData.Name
}
});
}
public Task Delete(FileData fileData)
public override async Task Move(FileData fileData, string newPath)
{
throw new NotImplementedException();
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 Task Move(FileData fileData, string newPath)
public override async Task Compress(params FileData[] files)
{
throw new NotImplementedException();
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 Task<string> GetLaunchUrl()
public override async Task Decompress(FileData fileData)
{
throw new NotImplementedException();
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,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

@@ -113,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

@@ -1,6 +1,15 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Helpers
@using Logging.Net
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using BlazorDownloadFile
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject AlertService AlertService
@inject SmartTranslateService SmartTranslateService
@inject IBlazorDownloadFileService FileService
@if (Editing)
{
@@ -14,35 +23,63 @@
}
else
{
<div class="card card-body mb-7">
<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 = "/";
<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>
}
<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">
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 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>
<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>
cp += path + "/";
}
<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>
@@ -53,35 +90,141 @@ else
<FileView @ref="View"
Access="Access"
ContextActions="Actions"
OnPathChanged="OnPathChanged"
OnElementClicked="OnElementClicked">
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 Access { get; set; }
public FileAccess Access { get; set; }
// File Editor
private bool Editing = false;
private string EditorInitialData = "";
private string EditorLanguage = "";
private FileData EditingFile;
private FileEditor Editor;
private FileView View;
private string CurrentPath = "/";
// File View
private FileView? View;
private ContextAction[] Actions =
// File Move
private FileAccess MoveAccess;
private FileSelectModal FileSelectModal;
private FileData? SingleMoveFile = null;
// Config
private ContextAction[] Actions = Array.Empty<ContextAction>();
protected override void OnInitialized()
{
new()
MoveAccess = (FileAccess)Access.Clone();
List<ContextAction> actions = new();
actions.Add(new()
{
Id = "rename",
Name = "Rename",
Action = (x) => { }
}
};
Action = async (x) =>
{
var name = await AlertService.Text(
SmartTranslateService.Translate("Rename"),
SmartTranslateService.Translate("Enter a new name"),
x.Name
);
if (name != x.Name)
{
await Access.Move(x, Access.CurrentPath + name);
}
await View!.Refresh();
}
});
actions.Add(new ()
{
Id = "download",
Name = "Download",
Action = async (x) =>
{
if (x.IsFile)
{
try
{
var stream = await Access.DownloadStream(x);
await ToastService.Info(SmartTranslateService.Translate("Starting download"));
await FileService.AddBuffer(stream);
await FileService.DownloadBinaryBuffers(x.Name);
}
catch (NotImplementedException)
{
var url = await Access.DownloadUrl(x);
NavigationManager.NavigateTo(url, true);
await ToastService.Info(SmartTranslateService.Translate("Starting download"));
}
}
}
});
actions.Add(new()
{
Id = "compress",
Name = "Compress",
Action = async (x) =>
{
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)
{
@@ -92,22 +235,13 @@ else
EditingFile = fileData;
Editing = true;
await InvokeAsync(StateHasChanged);
await InvokeAsync(StateHasChanged);
return true;
}
else
{
return false;
}
}
public async Task SetPath(string path)
{
await Access.SetDir(path);
CurrentPath = await Access.Pwd();
await InvokeAsync(StateHasChanged);
return false;
}
private async void Cancel(bool save = false)
@@ -122,9 +256,74 @@ else
await InvokeAsync(StateHasChanged);
}
private async void OnPathChanged(string path)
private async Task Launch()
{
CurrentPath = path;
var url = await Access.GetLaunchUrl();
NavigationManager.NavigateTo(url, true);
}
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()
{
await InvokeAsync(StateHasChanged);
}
private async Task StartMoveFiles()
{
await FileSelectModal.Show();
}
private async Task DeleteMultiple()
{
foreach (var data in View!.SelectedFiles)
{
await Access.Delete(data);
}
await View!.Refresh();
}
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,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

@@ -11,16 +11,19 @@
<thead>
<tr class="text-start text-gray-400 fw-bold fs-7 text-uppercase gs-0">
<th class="w-10px pe-2 sorting_disabled">
<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>
@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>
@@ -28,7 +31,7 @@
</table>
</div>
</div>
<div class="dataTables_scrollBody" style="position: relative; overflow: auto; max-height: 700px; width: 100%;">
<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">
@@ -38,7 +41,7 @@
<td>
<div class="d-flex align-items-center">
<span class="icon-wrapper">
<i class="bx bx-md bx-folder text-primary"></i>
<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>
@@ -58,20 +61,23 @@
{
<tr class="even">
<td class="w-10px">
<div class="form-check form-check-sm form-check-custom form-check-solid">
@{
var toggle = ToggleStatusCache.ContainsKey(file) && ToggleStatusCache[file];
}
@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>
@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">
@@ -92,17 +98,20 @@
<td class="text-end">
<div class="d-flex justify-content-end">
<div class="ms-2 me-7">
<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>
@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>
@@ -115,29 +124,46 @@
</div>
</div>
<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>
@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 IFileAccess Access { get; set; }
public FileAccess Access { get; set; }
[Parameter]
public Func<FileData, Task<bool>>? OnElementClicked { get; set; }
[Parameter]
public Action<string>? OnPathChanged { get; set; }
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();
@@ -145,8 +171,23 @@
public async Task Refresh()
{
Data = await Access.Ls();
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)
{
@@ -154,6 +195,7 @@
}
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task Load(LazyLoader arg)
@@ -169,6 +211,7 @@
ToggleStatusCache.Add(fileData, status);
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task SetToggleState(bool status)
@@ -181,6 +224,7 @@
}
await InvokeAsync(StateHasChanged);
OnSelectionChanged?.Invoke();
}
private async Task OnClicked(FileData fileData)
@@ -197,16 +241,25 @@
{
await Access.Cd(fileData.Name);
await Refresh();
OnPathChanged?.Invoke(await Access.Pwd());
}
}
private async Task GoUp()
{
if (OnElementClicked != null)
{
var canceled = await OnElementClicked.Invoke(new()
{
Name = "..",
IsFile = false
});
if (canceled)
return;
}
await Access.Up();
await Refresh();
OnPathChanged?.Invoke(await Access.Pwd());
}
private Task OnContextMenuClick(ItemClickEventArgs eventArgs)

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">
<FileManager2 FileAccess="FileAccess"></FileManager2>
</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

@@ -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,6 +8,7 @@
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Xterm
@using Moonlight.Shared.Components.ServerControl
@using Newtonsoft.Json
@@ -15,6 +16,10 @@
@inject ImageRepository ImageRepository
@inject ServerRepository ServerRepository
@inject WingsConsoleHelper WingsConsoleHelper
@inject MessageService MessageService
@inject NavigationManager NavigationManager
@implements IDisposable
<LazyLoader Load="LoadData">
@if (CurrentServer == null)
@@ -36,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">
@@ -72,7 +92,7 @@
case "network":
index = 3;
break;
case "plugins":
case "addons":
index = 4;
break;
case "settings":
@@ -207,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

@@ -5,10 +5,13 @@
@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">
<FileManager Access="FileAccess">
@@ -17,7 +20,10 @@
@code
{
private IFileAccess FileAccess;
[CascadingParameter]
public User User { get; set; }
private FileAccess FileAccess;
private Task Load(LazyLoader arg)
{
@@ -26,7 +32,7 @@
.Include(x => x.Node)
.First();
FileAccess = new WingsFileAccess(WingsApiHelper, WingsJwtHelper, server);
FileAccess = new WingsFileAccess(WingsApiHelper, WingsJwtHelper, server, ConfigService, User);
return Task.CompletedTask;
}

View File

@@ -411,3 +411,24 @@ Server not found;Server not found
A server with that id cannot be found or you have no access for this server;A server with that id cannot be found or you have no access for this server
Addons;Addons
Go up;Go up
Uploading files;Uploading files
complete;complete
Upload complete;Upload complete
Moving;Moving
selected;selected
Select folder to move the file(s) to;Select folder to move the file(s) to
Submit;Submit
Processing;Processing
Error from daemon;Error from daemon
Enter a new name;Enter a new name
The uploaded file should not be bigger than 100MB;The uploaded file should not be bigger than 100MB
Compress;Compress
Compressing;Compressing
Decompress;Decompress
Paper version;Paper version
Error creating server on wings;Error creating server on wings
Javascript version;Javascript version
Javascript file;Javascript file
Select javascript file to execute on start;Select javascript file to execute on start
Javascript Version;Javascript Version
Join2Start;Join2Start

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();
}
}