Improve User Interface

* File and folder pickers
* Menu
This commit is contained in:
ds5678 2024-01-04 00:33:43 -05:00
parent 069f6711f0
commit 48a165a923
8 changed files with 254 additions and 38 deletions

View File

@ -3,6 +3,7 @@
namespace AssetRipper.GUI.Web;
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(string[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

View File

@ -26,6 +26,7 @@
<ItemGroup>
<PackageReference Include="AssetRipper.Text.Html" Version="1.0.0" />
<PackageReference Include="NativeFileDialogs.Net" Version="1.1.0" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using AssetRipper.Web.Content;
using AssetRipper.GUI.Web.Paths;
using AssetRipper.Web.Content;
namespace AssetRipper.GUI.Web;
public abstract class DefaultPage : HtmlPage
@ -6,7 +7,7 @@ public abstract class DefaultPage : HtmlPage
public sealed override void Write(TextWriter writer)
{
base.Write(writer);
using (new Html(writer).WithLang(Localizations.Localization.CurrentLanguageCode).End())
using (new Html(writer).WithLang(Localization.CurrentLanguageCode).End())
{
using (new Head(writer).End())
{
@ -20,37 +21,83 @@ public abstract class DefaultPage : HtmlPage
{
using (new Header(writer).End())
{
using (new Nav(writer).WithClass("navbar navbar-expand-sm navbar-toggleable-sm border-bottom box-shadow mb-3").End())
using (new Div(writer).WithClass("btn-group").End())
{
using (new Div(writer).WithClass("container").End())
using (new Div(writer).WithClass("btn-group dropdown").End())
{
new A(writer).WithClass("navbar-brand").WithHref("/").Close(GameFileLoader.Premium ? Localization.AssetRipperPremium : Localization.AssetRipperFree);
using (new Button(writer)
.WithClass("navbar-toggler")
.WithType("button")
.WithCustomAttribute("data-bs-toggle", "collapse")
.WithCustomAttribute("data-bs-target", ".navbar-collapse")
.WithCustomAttribute("aria-controls", "navbarSupportedContent")
.WithCustomAttribute("aria-expanded", "false")
.WithCustomAttribute("aria-label", "Toggle navigation").End())
WriteDropdownButton(writer, Localization.MenuFile);
using (new Ul(writer).WithClass("dropdown-menu").End())
{
new Span(writer).WithClass("navbar-toggler-icon").Close();
}
using (new Div(writer).WithClass("navbar-collapse collapse d-sm-inline-flex justify-content-between").End())
{
using (new Ul(writer).WithClass("navbar-nav flex-grow-1").End())
using (new Li(writer).End())
{
using (new Li(writer).WithClass("nav-item").End())
WritePostLink(writer, "/LoadFile", Localization.MenuFileOpenFile, "dropdown-item");
}
using (new Li(writer).End())
{
WritePostLink(writer, "/LoadFolder", Localization.MenuFileOpenFolder, "dropdown-item");
}
using (new Li(writer).End())
{
WritePostLink(writer, "/Reset", Localization.MenuFileReset, "dropdown-item");
}
using (new Li(writer).End())
{
new Hr(writer).WithClass("dropdown-divider").Close();
}
using (new Li(writer).End())
{
new A(writer).WithClass("dropdown-item").WithHref("/Settings/Edit").Close(Localization.Settings);
}
}
}
using (new Div(writer).WithClass("btn-group dropdown").End())
{
WriteDropdownButton(writer, "View");
using (new Ul(writer).WithClass("dropdown-menu").End())
{
using (new Li(writer).End())
{
new A(writer).WithClass("dropdown-item").WithHref("/").Close(Localization.Home);
}
using (new Li(writer).End())
{
new A(writer).WithClass("dropdown-item").WithHref("/Settings/Edit").Close(Localization.Settings);
}
using (new Li(writer).End())
{
new A(writer).WithClass("dropdown-item").WithHref("/Commands").Close(Localization.Commands);
}
using (new Li(writer).End())
{
new A(writer).WithClass("dropdown-item").WithHref("/Privacy").Close(Localization.Privacy);
}
using (new Li(writer).End())
{
new A(writer).WithClass("dropdown-item").WithHref("/Licenses").Close(Localization.Licenses);
}
}
}
using (new Div(writer).WithClass("btn-group dropdown").End())
{
WriteDropdownButton(writer, Localization.MenuExport);
using (new Ul(writer).WithClass("dropdown-menu").End())
{
using (new Li(writer).End())
{
WritePostLink(writer, "/Export", Localization.MenuExportAll, "dropdown-item");
}
}
}
using (new Div(writer).WithClass("btn-group dropdown").End())
{
WriteDropdownButton(writer, Localization.MenuLanguage);
using (new Ul(writer).WithClass("dropdown-menu").End())
{
foreach ((string code, string name) in Localizations.LocalizationLoader.LanguageNameDictionary)
{
using (new Li(writer).End())
{
new A(writer).WithClass("nav-link").WithHref("/").Close(Localization.Home);
}
using (new Li(writer).WithClass("nav-item").End())
{
new A(writer).WithClass("nav-link").WithHref("/Settings/Edit").Close("Settings");
}
using (new Li(writer).WithClass("nav-item").End())
{
new A(writer).WithClass("nav-link").WithHref("/Commands").Close("Commands");
WritePostLink(writer, $"/Localization?code={code}", name, "dropdown-item");
}
}
}
@ -81,6 +128,23 @@ public abstract class DefaultPage : HtmlPage
}
}
private static void WriteDropdownButton(TextWriter writer, string buttonText)
{
new Button(writer).WithClass("btn btn-dark dropdown-toggle mx-0")
.WithType("button")
.WithCustomAttribute("data-bs-toggle", "dropdown")
.WithCustomAttribute("aria-expanded", "false")
.Close(buttonText);
}
private static void WritePostLink(TextWriter writer, string url, string name, string? @class = null)
{
using (new Form(writer).WithAction(url).WithMethod("post").End())
{
new Input(writer).WithType("submit").WithClass(@class).WithValue(name.ToHtml()).Close();
}
}
public abstract string? GetTitle();
public abstract void WriteInnerContent(TextWriter writer);

View File

@ -0,0 +1,86 @@
using AssetRipper.Web.Extensions;
using Microsoft.AspNetCore.Http;
using NativeFileDialogs.Net;
namespace AssetRipper.GUI.Web;
internal static class Dialogs
{
private static readonly object lockObject = new();
public static class OpenFiles
{
public static Task HandleGetRequest(HttpContext context)
{
context.Response.DisableCaching();
NfdStatus status = GetUserInput(out string[]? paths);
//Maybe do something else when user cancels the dialog?
return Results.Json(paths ?? [], AppJsonSerializerContext.Default.StringArray).ExecuteAsync(context);
}
public static NfdStatus GetUserInput(out string[]? paths, IDictionary<string, string>? filters = null, string? defaultPath = null)
{
lock (lockObject)
{
return Nfd.OpenDialogMultiple(out paths, filters, defaultPath);
}
}
}
public static class OpenFile
{
public static Task HandleGetRequest(HttpContext context)
{
context.Response.DisableCaching();
NfdStatus status = GetUserInput(out string? path);
//Maybe do something else when user cancels the dialog?
return Results.Json(path ?? "", AppJsonSerializerContext.Default.String).ExecuteAsync(context);
}
public static NfdStatus GetUserInput(out string? path, IDictionary<string, string>? filters = null, string? defaultPath = null)
{
lock (lockObject)
{
return Nfd.OpenDialog(out path, filters, defaultPath);
}
}
}
public static class OpenFolder
{
public static Task HandleGetRequest(HttpContext context)
{
context.Response.DisableCaching();
NfdStatus status = GetUserInput(out string? path);
//Maybe do something else when user cancels the dialog?
return Results.Json(path ?? "", AppJsonSerializerContext.Default.String).ExecuteAsync(context);
}
public static NfdStatus GetUserInput(out string? path, string? defaultPath = null)
{
lock (lockObject)
{
return Nfd.PickFolder(out path, defaultPath);
}
}
}
public static class SaveFile
{
public static Task HandleGetRequest(HttpContext context)
{
context.Response.DisableCaching();
NfdStatus status = GetUserInput(out string? path);
//Maybe do something else when user cancels the dialog?
return Results.Json(path ?? "", AppJsonSerializerContext.Default.String).ExecuteAsync(context);
}
public static NfdStatus GetUserInput(out string? path, IDictionary<string, string>? filters = null, string defaultName = "Untitled", string? defaultPath = null)
{
lock (lockObject)
{
return Nfd.SaveDialog(out path, filters, defaultName, defaultPath);
}
}
}
}

View File

@ -1,34 +1,76 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace AssetRipper.GUI.Web.Pages;
public static class Commands
{
public readonly struct Load : ICommand
public readonly struct LoadFile : ICommand
{
static Task ICommand.Start(HttpRequest request)
static async Task ICommand.Start(HttpRequest request)
{
string? path = request.Form["Path"];
IFormCollection form = await request.ReadFormAsync();
string[]? paths;
if (form.TryGetValue("Path", out StringValues values))
{
paths = values;
}
else
{
Dialogs.OpenFiles.GetUserInput(out paths);
}
if (paths is { Length: > 0 })
{
GameFileLoader.LoadAndProcess(paths);
}
}
}
public readonly struct LoadFolder : ICommand
{
static async Task ICommand.Start(HttpRequest request)
{
IFormCollection form = await request.ReadFormAsync();
string? path;
if (form.TryGetValue("Path", out StringValues values))
{
path = values;
}
else
{
Dialogs.OpenFolder.GetUserInput(out path);
}
if (!string.IsNullOrEmpty(path))
{
GameFileLoader.LoadAndProcess([path]);
}
return Task.CompletedTask;
}
}
public readonly struct Export : ICommand
{
static Task ICommand.Start(HttpRequest request)
static async Task ICommand.Start(HttpRequest request)
{
string? path = request.Form["Path"];
IFormCollection form = await request.ReadFormAsync();
string? path;
if (form.TryGetValue("Path", out StringValues values))
{
path = values;
}
else
{
Dialogs.OpenFolder.GetUserInput(out path);
}
if (!string.IsNullOrEmpty(path))
{
GameFileLoader.Export(path);
}
return Task.CompletedTask;
}
}

View File

@ -26,7 +26,7 @@ public sealed class CommandsPage : DefaultPage
{
using (new P(writer).End())
{
WritePicker(writer, "/Load", Localization.MenuLoad, "btn btn-primary");
WritePicker(writer, "/LoadFile", Localization.MenuLoad, "btn btn-primary");
}
}
}

View File

@ -6,7 +6,7 @@ public sealed class IndexPage : DefaultPage
{
public static IndexPage Instance { get; } = new();
public override string? GetTitle() => "AssetRipper";
public override string? GetTitle() => GameFileLoader.Premium ? Localization.AssetRipperPremium : Localization.AssetRipperFree;
public override void WriteInnerContent(TextWriter writer)
{

View File

@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using System.Diagnostics;
namespace AssetRipper.GUI.Web;
@ -98,11 +99,32 @@ public static class WebApplicationLauncher
app.MapPost("/Resources/View", Pages.Resources.ViewPage.HandlePostRequest);
app.MapPost("/Scenes/View", Pages.Scenes.ViewPage.HandlePostRequest);
app.MapPost("/Localization", (context) =>
{
context.Response.DisableCaching();
if (context.Request.Query.TryGetValue("code", out StringValues code))
{
string? language = code;
if (language is not null && LocalizationLoader.LanguageNameDictionary.ContainsKey(language))
{
Localization.LoadLanguage(language);
}
}
return Results.Redirect("/").ExecuteAsync(context);
});
//Commands
app.MapPost("/Export", Commands.HandleCommand<Commands.Export>);
app.MapPost("/Load", Commands.HandleCommand<Commands.Load>);
app.MapPost("/LoadFile", Commands.HandleCommand<Commands.LoadFile>);
app.MapPost("/LoadFolder", Commands.HandleCommand<Commands.LoadFolder>);
app.MapPost("/Reset", Commands.HandleCommand<Commands.Reset>);
//Dialogs
app.MapGet("/Dialogs/SaveFile", Dialogs.SaveFile.HandleGetRequest);
app.MapGet("/Dialogs/OpenFolder", Dialogs.OpenFolder.HandleGetRequest);
app.MapGet("/Dialogs/OpenFile", Dialogs.OpenFile.HandleGetRequest);
app.MapGet("/Dialogs/OpenFiles", Dialogs.OpenFiles.HandleGetRequest);
app.Run();
}