Skip to content

Commit

Permalink
Web: Add Text Editor
Browse files Browse the repository at this point in the history
  • Loading branch information
xoascf committed Dec 19, 2023
1 parent 6992841 commit d816c4d
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 10 deletions.
176 changes: 176 additions & 0 deletions OTRMod.Web/Pages/TextEditor.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
@page "/text-editor"
@using BlazorDownloadFile
@using OTRMod.Web.Shared.Components
@using OTRMod.OTR
@using OTRMod.Utility

<PageTitle>OTRMod Web - Text Editor (OoT)</PageTitle>
<h3>Text Editor (OoT)</h3>
Import and export compatible game text formats.
<br />
<br />
<Accordion Id="selectionAccordion">
<AccordionItem Header="Load from OTR" Id="loadOtr" IsInitiallyOpen=true>
<InputFile class="form-control" type="file" id="otrFile" OnChange="@((InputFileChangeEventArgs e) => ProcessInput(e, true))" accept=".otr" single />
</AccordionItem>
<AccordionItem Header="Load from .h message file" Id="loadH">
<InputFile class="form-control" type="file" id="zretTextFile" OnChange="@((InputFileChangeEventArgs e) => ProcessInput(e, false))" accept=".h" single />
</AccordionItem>
</Accordion>
<br />
<div class="row g-3" visi>
<div class="col-sm-3" style="width: 190px;">
<label for="charMap" class="form-label">Replacements:</label>
<InputTextArea class="form-control" id="charMap" rows="14" cols="16" @bind-Value="@charReplacements" @oninput="LoadTextArray" disabled="@_working"></InputTextArea>
</div>
<div class="col">
<label for="tabMessageFiles" class="form-label">Text Editor:</label>
<Tabs CssClass="border-0" TabList="@tabList" Id="tabMessageFiles" />
@if (tabList.Count != _textFiles.Count)
foreach (KeyValuePair<string, OTRMod.Z.Text> msg in _textFiles)
tabList.Add(new TextDataItem { Id = msg.Key, Title = msg.Key, Content =@<p>Loading...</p>, OnSelected = EventCallback.Factory.Create(this, (Tabs.TabItem e) => OnTabSelected(e)) });
</div>
</div>
<br />
<label for="msgPath" class="form-label">Output OTR message path:</label>
<input class="form-control" list="msgPaths" id="msgPath" @bind="@_msgPathInOTR" aria-describedby="validationMsgPath" required />
<div id="validationMsgPath" class="invalid-feedback">Message OTR path cannot be blank!</div>
<datalist id="msgPaths">
<option value="@DEFAULT_MESSAGE_OTR_PATH">
</option>
<option value="text/ger_message_data_static/ger_message_data_static">
</option>
<option value="text/fra_message_data_static/fra_message_data_static">
</option>
<option value="text/staff_message_data_static/staff_message_data_static">
</option>
</datalist>
<br />
<button class="btn btn-primary" type="submit" @onclick="ExportToOTR" disabled="@(currentTab == null || !currentTab.IsLoaded)">
Save OTR file
</button>
<button class="btn btn-primary" type="submit" @onclick="GetH" disabled="@(currentTab == null || !currentTab.IsLoaded)">
Save .h message file
</button>
<br />
@code {
public class TextDataItem : Tabs.TabItem {
public bool IsLoaded { get; set; }
public string PreContent { get; set; }
}

[Inject] private IBlazorDownloadFileService DlFileService { get; set; } = null!;
private MemoryStream _otrMs = new();
private List<TextDataItem> tabList = new List<TextDataItem> { };
private Dictionary<string, OTRMod.Z.Text> _textFiles = new();
private Dictionary<string, string> _replacements = new();
private bool _working = false;
private const string DEFAULT_MESSAGE_OTR_PATH = "text/nes_message_data_static/nes_message_data_static";
private string _msgPathInOTR = DEFAULT_MESSAGE_OTR_PATH;
private string currentHContent = "";
private bool _needReplacements = false;
private TextDataItem? currentTab;
private string charReplacements = DecodeBase64("XG49MDEK4oC+PTdGCsOAPTgwCsOuPTgxCsOCPTgyCsOEPTgzCsOHPTg0CsOIPTg1CsOJPTg2CsOKPTg3CsOLPTg4CsOPPTg5CsOUPThBCsOWPThCCsOZPThDCsObPThECsOcPThFCsOfPThGCsOgPTkwCsOhPTkxCsOiPTkyCsOkPTkzCsOnPTk0CsOoPTk1CsOpPTk2CsOqPTk3CsOrPTk4CsOvPTk5CsO0PTlBCsO2PTlCCsO5PTlDCsO7PTlECsO8PTlFCltBXT05RgpbQl09QTAKW0NdPUExCltMXT1BMgpbUl09QTMKW1pdPUE0CltDLVVwXT1BNQpbQy1Eb3duXT1BNgpbQy1MZWZ0XT1BNwpbQy1SaWdodF09QTgK4pa8PUE5CltDb250cm9sLVBhZF09QUEKW0QtUGFkXT1BQg==");

RenderFragment LoadMsg() {
return @<InputTextArea class="form-control" rows="12" cols="96" @bind-Value="@currentHContent" disabled="@_working" style="border-radius: 0 var(--bs-border-radius) var(--bs-border-radius);"></InputTextArea>;
}

private void OnTabSelected(Tabs.TabItem e) {
if (currentTab != null)
currentTab.PreContent = currentHContent;

var tab = e as TextDataItem;
Console.WriteLine($"Tab selected: {tab.Id}");
if (!tab.IsLoaded || _needReplacements) {
tab.PreContent = _textFiles[tab.Id].ToHumanReadable(_replacements);
_needReplacements = false;
tab.IsLoaded = true;
}

currentHContent = tab.PreContent;
_msgPathInOTR = $"text/{tab.Title}/{tab.Title}";
tab.Content = LoadMsg();
currentTab = tab;
}

private static string DecodeBase64(string base64String) {
byte[] data = Convert.FromBase64String(base64String);
return System.Text.Encoding.UTF8.GetString(data);
}

private void LoadTextArray(ChangeEventArgs e) {
charReplacements = e.Value.ToString();
SetReplacements();
}

private void SetReplacements() {
var charMapTemp = OTRMod.ID.Text.LoadCharMap(charReplacements.ToStringArray());
var diffKeys = charMapTemp.Where(kv => !_replacements.ContainsKey(kv.Key) || !_replacements[kv.Key].Equals(kv.Value)).ToDictionary(kv => kv.Key, kv => kv.Value);

if (diffKeys.Any()) {
_replacements = charMapTemp;
_needReplacements = true;
Console.WriteLine($"Replacements updated:");
foreach (var kv in diffKeys)
Console.WriteLine($"Key: {kv.Key}, Value: {kv.Value}");
}
}

protected override void OnInitialized() {
SetReplacements();
}

private async void ProcessInput(InputFileChangeEventArgs e, bool isOTR) {
IBrowserFile inputFile = e.File;
tabList.Clear();
_textFiles.Clear();

MemoryStream dataMs = new();
if (isOTR) {
await inputFile.OpenReadStream(0x4000000).CopyToAsync(dataMs); // is 64 MiB too much for your OTR?
Dictionary<string, Stream> msgResFiles = new();
await Task.Delay(10);
Load.OnlyFrom("message_data_static", dataMs, ref msgResFiles);
foreach (var msgRes in msgResFiles) {
await Task.Delay(10);
var otrRes = OTRMod.Z.Resource.Read(((MemoryStream)msgRes.Value).ToArray());
var otrResText = OTRMod.Z.Text.LoadFrom(otrRes);
var otrResFileName = Path.GetFileName(msgRes.Key);

_textFiles.Add(otrResFileName, otrResText);
msgRes.Value.Flush();
}
}
else {
await inputFile.OpenReadStream(0x100000).CopyToAsync(dataMs); // 1 MiB is more than enough for .h files
var otrResText = new OTRMod.Z.Text(dataMs.ToArray(), _replacements);
var otrResFileName = Path.GetFileNameWithoutExtension(inputFile.Name);

_textFiles.Add(otrResFileName, otrResText);
}

StateHasChanged();

dataMs.Flush();
}

private async Task GetH() {
_working = true;
await Task.Delay(10);
string hFileName = $"{currentTab.Title}.h";
await DlFileService.DownloadFileFromText(hFileName, currentHContent, System.Text.Encoding.UTF8, "text/plain");
_working = false;
}

private async Task ExportToOTR() {
_working = true;
_otrMs.SetLength(0);
var otrResText = new OTRMod.Z.Text(currentHContent, _replacements);
await Task.Delay(10);
Generate.AddFile(_msgPathInOTR, otrResText.Formatted());
Generate.FromImage(ref _otrMs);
await DlFileService.DownloadFile("GeneratedMessages.otr", _otrMs, "application/octet-stream");
_working = false;
}
}
27 changes: 27 additions & 0 deletions OTRMod.Web/Shared/Components/Accordion.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@inherits ComponentBase

<div class="accordion" id="@Id">
<CascadingValue Value="@this" Name="ParentAccordion">
@ChildContent
</CascadingValue>
</div>

@code {
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public string Id { get; set; } = "accordion";

private string selectedItem = null;

public void NotifyItemSelection(string itemId) {
if (selectedItem == itemId)
selectedItem = null; // Collapse the currently selected item if clicked again
else
selectedItem = itemId;

StateHasChanged();
}

public bool IsItemCollapsed(string itemId) {
return selectedItem != itemId;
}
}
37 changes: 37 additions & 0 deletions OTRMod.Web/Shared/Components/AccordionItem.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@inherits ComponentBase

<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button@(ParentAccordion.IsItemCollapsed(Id) ? " collapsed" : "")"
type="button"
data-bs-toggle="collapse"
data-bs-target="@($"#{Id}")"
aria-expanded="@(!ParentAccordion.IsItemCollapsed(Id))"
aria-controls="@Id"
@onclick="ToggleCollapse">
@Header
</button>
</h2>
<div id="@Id" class="accordion-collapse collapse@(ParentAccordion.IsItemCollapsed(Id) ? "" : " show")" aria-labelledby="@($"heading{Id}")">
<div class="accordion-body">
@ChildContent
</div>
</div>
</div>

@code {
[CascadingParameter(Name = "ParentAccordion")] public Accordion ParentAccordion { get; set; }
[Parameter] public string Header { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public string Id { get; set; }
[Parameter] public bool IsInitiallyOpen { get; set; } = false;

protected override void OnInitialized() {
if (IsInitiallyOpen)
ParentAccordion.NotifyItemSelection(Id);
}

private void ToggleCollapse() {
ParentAccordion.NotifyItemSelection(Id);
}
}
60 changes: 60 additions & 0 deletions OTRMod.Web/Shared/Components/Tabs.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@using System.Collections.Generic
@using Microsoft.AspNetCore.Components

<div class="nav nav-tabs @CssClass" id="@Id" role="tablist">
@foreach (var tab in TabList) {
if (tab == selectedTab && isEditingTitle) {
<input class="nav-link" @ondblclick="StopEditingTitle" @onblur="StopEditingTitle" @bind="tab.Title" />
} else {
<button class="nav-link @GetTabClass(tab)" id="@($"{Id}-tab")" @onclick="() => SelectTab(tab)" @ondblclick="() => StartEditingTitle(tab)">
@tab.Title
</button>
}
}
</div>

<div class="tab-content" id="@($"{Id}Content")">
@foreach (var tab in TabList) {
<div class="tab-pane @GetTabContentClass(tab)" id="@Id" role="tabpanel">
@tab.Content
</div>
}
</div>

@code {
public class TabItem {
[Parameter] public string Id { get; set; }

Check warning on line 26 in OTRMod.Web/Shared/Components/Tabs.razor

View workflow job for this annotation

GitHub Actions / deploy

Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
[Parameter] public string Title { get; set; }

Check warning on line 27 in OTRMod.Web/Shared/Components/Tabs.razor

View workflow job for this annotation

GitHub Actions / deploy

Non-nullable property 'Title' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
[Parameter] public RenderFragment Content { get; set; }

Check warning on line 28 in OTRMod.Web/Shared/Components/Tabs.razor

View workflow job for this annotation

GitHub Actions / deploy

Non-nullable property 'Content' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
[Parameter] public EventCallback<TabItem> OnSelected { get; set; }
}
[Parameter] public string CssClass { get; set; } = "";
[Parameter] public IEnumerable<TabItem> TabList { get; set; }

Check warning on line 32 in OTRMod.Web/Shared/Components/Tabs.razor

View workflow job for this annotation

GitHub Actions / deploy

Non-nullable property 'TabList' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
[Parameter] public string Id { get; set; } = "tabs";

private TabItem selectedTab;

Check warning on line 35 in OTRMod.Web/Shared/Components/Tabs.razor

View workflow job for this annotation

GitHub Actions / deploy

Non-nullable field 'selectedTab' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

private string GetTabClass(TabItem tab) {
return tab == selectedTab ? "active" : null;

Check warning on line 38 in OTRMod.Web/Shared/Components/Tabs.razor

View workflow job for this annotation

GitHub Actions / deploy

Possible null reference return.
}

private string GetTabContentClass(TabItem tab) {
return tab == selectedTab ? "active show" : null;

Check warning on line 42 in OTRMod.Web/Shared/Components/Tabs.razor

View workflow job for this annotation

GitHub Actions / deploy

Possible null reference return.
}

private async Task SelectTab(TabItem tab) {
selectedTab = tab;
await tab.OnSelected.InvokeAsync(tab);
}

private bool isEditingTitle;

private void StartEditingTitle(TabItem tab) {
selectedTab = tab;
isEditingTitle = true;
}

private void StopEditingTitle() {
isEditingTitle = false;
}
}
3 changes: 3 additions & 0 deletions OTRMod.Web/Shared/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
<li class="nav-item px-3">
<NavLink class="nav-link px-3" href=""><i class="bi pe-none me-2 fa-solid fa-download"></i>@T["get_otr_tab"]</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link px-3" href="text-editor"><i class="bi pe-none me-2 fa-solid fa-edit"></i>Text Editor</NavLink>
</li>
</ul>
</div>

Expand Down
26 changes: 19 additions & 7 deletions OTRMod/ID/Text.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,35 @@ public static string EvalCodes(string input) {

return sb.ToString().Replace("\u201C", "\"");
}

public static string Endec(byte[] data, bool bin, StringsDict replacements) {
string t = bin ? Encoding.UTF8.GetString(data) : SturmScharf.EncodingProvider.Latin1.GetString(data);

public static string Endec(string t, bool bin, StringsDict replacements) {
foreach (KeyValuePair<string, string> rep in replacements)
t = bin ? t.Replace(rep.Key, rep.Value) : t.Replace(rep.Value, rep.Key);

return t;
}

public static string Endec(byte[] data, bool bin, StringsDict replacements) {
string t = bin ? Encoding.UTF8.GetString(data) : SturmScharf.EncodingProvider.Latin1.GetString(data);

return Endec(t, bin, replacements);
}

public static StringsDict LoadCharMap(string[] lines) {
StringsDict cm = new();

foreach (string line in lines) {
string[] splitLine = line.Split('=');
string key = splitLine[0];
string val = Convert.ToChar(Convert.ToUInt32(splitLine[1], 16)).ToString();
cm.Add(key, val);
if (!line.Contains("="))
continue;

string[] rLine = line.Split(new char[] { '=' }, 2);
if (rLine[0].IsNullOrEmpty() || rLine[1].IsNullOrEmpty())
continue;

if (uint.TryParse(rLine[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint val))
if (val > 0x10FFFF)
continue;
cm.Add(rLine[0], char.ConvertFromUtf32((int)val));
}

return cm;
Expand Down
14 changes: 14 additions & 0 deletions OTRMod/OTR/Load.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace OTRMod.OTR;

public static class Load {
// All from...
public static void From(Stream s, ref Dictionary<string, Stream> files) {
using MpqArchive archive = MpqArchive.Open(s, true);
foreach (MpqFile file in archive.GetMpqFiles())
Expand All @@ -20,4 +21,17 @@ public static void From(Stream s, ref Dictionary<string, Stream> files) {
files.Add(kf.FileName, dataStream);
}
}

// Search like...
public static void OnlyFrom
(string fileName, Stream s, ref Dictionary<string, Stream> files) {
using MpqArchive archive = MpqArchive.Open(s, true);
foreach (MpqFile file in archive.GetMpqFiles())
if (file is MpqKnownFile kf && kf.FileName.Contains(fileName)) {
Stream dataStream = new MemStream();
kf.MpqStream.CopyTo(dataStream);
kf.MpqStream.Close();
files.Add(kf.FileName, dataStream);
}
}
}
6 changes: 5 additions & 1 deletion OTRMod/Utility/ByteArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,12 @@ public static byte[] CopyAs(this byte[] a, ByteOrder f, int s = 0, int l = 4) {

internal static readonly string[] Separators = { "\n", "\r\n", "\r" };

public static string[] ToStringArray(this string text) {
return text.Split(Separators, StringSplitOptions.None);
}

public static string[] ToStringArray(this byte[] data, Encoding? encoding = null) {
encoding ??= SturmScharf.EncodingProvider.Latin1;
return encoding.GetString(data).Split(Separators, StringSplitOptions.None);
return encoding.GetString(data).ToStringArray();
}
}
Loading

0 comments on commit d816c4d

Please sign in to comment.