Improved FileButton behavior, opening the same file twice in a row will now work. Also simplified its implementation.

This commit is contained in:
Daan Vanden Bosch 2020-01-05 23:20:53 +01:00
parent cb41529518
commit 3edc9b857d
17 changed files with 84 additions and 140 deletions

View File

@ -3,9 +3,11 @@
Write-Output "Running tests."
yarn test
if ($LastExitCode -ne 0) { throw "Tests failed." }
Write-Output "Generating production build."
yarn build
if ($LastExitCode -ne 0) { throw "Build failed." }
Write-Output "Deleting ./deployment contents."
Remove-Item -Recurse ./deployment/*
@ -22,9 +24,13 @@ Write-Output $version > version.txt
Write-Output "Committing and pushing to gh-pages."
Set-Location ./deployment
git pull
if ($LastExitCode -ne 0) { throw "Git pull failed." }
git add .
if ($LastExitCode -ne 0) { throw "Git add failed." }
git commit -m "Release $version."
if ($LastExitCode -ne 0) { throw "Git commit failed." }
git push
if ($LastExitCode -ne 0) { throw "Git push failed." }
Set-Location ..

35
src/core/files.ts Normal file
View File

@ -0,0 +1,35 @@
import { input } from "./gui/dom";
export function open_files(options?: { accept?: string; multiple?: boolean }): Promise<File[]> {
return new Promise((resolve) => {
const el = input({ type: "file" });
el.accept = options?.accept ?? "";
el.multiple = options?.multiple ?? false;
el.onchange = () => {
if (el.files && el.files.length) {
resolve([...el.files]);
} else {
resolve([]);
}
};
el.click();
});
}
export function read_file(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("loadend", () => {
if (reader.result instanceof ArrayBuffer) {
resolve(reader.result);
} else {
reject(new Error("Couldn't read file."));
}
});
reader.readAsArrayBuffer(file);
});
}

View File

@ -1,11 +0,0 @@
@import "./Button.css";
.core_FileButton_input {
overflow: hidden;
clip: rect(0, 0, 0, 0);
position: absolute;
width: 1px;
height: 1px;
border: none;
padding: 0;
}

View File

@ -1,82 +1,30 @@
import { icon, Icon, input, label, span } from "./dom";
import "./FileButton.css";
import { property } from "../observable";
import { Property } from "../observable/property/Property";
import { Control, ControlOptions } from "./Control";
import { WritableProperty } from "../observable/property/WritableProperty";
import { Button, ButtonOptions } from "./Button";
import { open_files } from "../files";
export type FileButtonOptions = ControlOptions & {
export type FileButtonOptions = ButtonOptions & {
accept?: string;
multiple?: boolean;
icon_left?: Icon;
};
export class FileButton extends Control {
readonly element = label({
className: "core_FileButton core_Button",
});
readonly files: Property<File[]>;
private input: HTMLInputElement = input({
className: "core_FileButton_input core_Button_inner",
});
export class FileButton extends Button {
private readonly _files: WritableProperty<File[]> = property<File[]>([]);
constructor(text: string, options?: FileButtonOptions) {
readonly files: Property<File[]> = this._files;
constructor(options?: FileButtonOptions) {
super(options);
this.files = this._files;
this.input.type = "file";
this.input.onchange = () => {
if (this.input.files && this.input.files.length) {
this._files.val = [...this.input.files!];
} else {
this._files.val = [];
}
};
const inner_element = span({
className: "core_FileButton_inner core_Button_inner",
});
if (options) {
if (options.accept != undefined) this.input.accept = options.accept;
if (options.multiple != undefined) this.input.multiple = options.multiple;
if (options.icon_left != undefined) {
inner_element.append(
span(
{ className: "core_FileButton_left core_Button_left" },
icon(options.icon_left),
),
);
}
}
inner_element.append(span({ className: "core_Button_center" }, text));
this.element.append(inner_element, this.input);
this.element.classList.add("core_FileButton");
this.disposables(
this.enabled.observe(({ value }) => {
this.input.disabled = !value;
if (value) {
this.element.classList.remove("disabled");
} else {
this.element.classList.add("disabled");
}
this.click.observe(async () => {
this._files.val = await open_files(options);
}),
);
this.finalize_construction();
}
click(): void {
this.input.click();
}
}

View File

@ -1,15 +0,0 @@
export async function read_file(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("loadend", () => {
if (reader.result instanceof ArrayBuffer) {
resolve(reader.result);
} else {
reject(new Error("Couldn't read file."));
}
});
reader.readAsArrayBuffer(file);
});
}

View File

@ -8,7 +8,7 @@ import { undo_manager } from "../../core/undo/UndoManager";
import { Controller } from "../../core/controllers/Controller";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { create_new_quest } from "../stores/quest_creation";
import { read_file } from "../../core/read_file";
import { open_files, read_file } from "../../core/files";
import {
parse_bin_dat_to_quest,
parse_qst_to_quest,
@ -19,7 +19,6 @@ import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCur
import { Endianness } from "../../core/data_formats/Endianness";
import { convert_quest_from_model, convert_quest_to_model } from "../stores/model_conversion";
import { LogManager } from "../../core/Logger";
import { input } from "../../core/gui/dom";
import { basename } from "../../core/util";
const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController");
@ -102,14 +101,9 @@ export class QuestEditorToolBarController extends Controller {
this.quest_filename = undefined;
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", () => {
const input_element = input();
input_element.type = "file";
input_element.multiple = true;
input_element.onchange = () => {
this.open_files(Array.prototype.slice.apply(input_element.files));
};
input_element.click();
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", async () => {
const files = await open_files({ accept: ".bin, .dat, .qst", multiple: true });
this.parse_files(files);
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Shift-S", this.save_as),
@ -140,7 +134,7 @@ export class QuestEditorToolBarController extends Controller {
this.quest_editor_store.set_current_quest(create_new_quest(this.area_store, episode));
// TODO: notify user of problems.
open_files = async (files: File[]): Promise<void> => {
parse_files = async (files: File[]): Promise<void> => {
try {
if (files.length === 0) return;

View File

@ -19,8 +19,9 @@ export class QuestEditorToolBar extends ToolBar {
items: [Episode.I],
to_label: episode => `Episode ${Episode[episode]}`,
});
const open_file_button = new FileButton("Open file...", {
const open_file_button = new FileButton({
icon_left: Icon.File,
text: "Open file...",
accept: ".bin, .dat, .qst",
multiple: true,
tooltip: "Open a quest file (Ctrl-O)",
@ -108,7 +109,7 @@ export class QuestEditorToolBar extends ToolBar {
this.disposables(
new_quest_button.chosen.observe(({ value: episode }) => ctrl.create_new_quest(episode)),
open_file_button.files.observe(({ value: files }) => ctrl.open_files(files)),
open_file_button.files.observe(({ value: files }) => ctrl.parse_files(files)),
save_as_button.click.observe(ctrl.save_as),
save_as_button.enabled.bind_to(ctrl.can_save),

View File

@ -55,15 +55,15 @@ exports[`Renders correctly. 1`] = `
</div>
</div>
</div>
<label
class="core_FileButton core_Button"
<button
class="core_Button core_FileButton"
title="Open a quest file (Ctrl-O)"
>
<span
class="core_FileButton_inner core_Button_inner"
class="core_Button_inner"
>
<span
class="core_FileButton_left core_Button_left"
class="core_Button_left"
>
<span>
<span
@ -77,13 +77,7 @@ exports[`Renders correctly. 1`] = `
Open file...
</span>
</span>
<input
accept=".bin, .dat, .qst"
class="core_FileButton_input core_Button_inner"
multiple=""
type="file"
/>
</label>
</button>
<button
class="core_Button disabled"
disabled=""

View File

@ -1,6 +1,6 @@
import { Controller } from "../../core/controllers/Controller";
import { filename_extension } from "../../core/util";
import { read_file } from "../../core/read_file";
import { read_file } from "../../core/files";
import { parse_xvm, XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness";

View File

@ -6,7 +6,7 @@ import { SectionId } from "../../../core/model";
export class CharacterClassOptionsController extends Controller {
readonly enabled: Property<boolean>;
readonly current_section_id: Property<SectionId>;
readonly current_section_id: Property<SectionId | undefined>;
readonly current_body_options: Property<readonly number[]>;
readonly current_body: Property<number | undefined>;

View File

@ -1,7 +1,7 @@
import { Controller } from "../../../core/controllers/Controller";
import { Property } from "../../../core/observable/property/Property";
import { ModelStore } from "../../stores/ModelStore";
import { read_file } from "../../../core/read_file";
import { read_file } from "../../../core/files";
import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../../core/data_formats/Endianness";
import { parse_nj, parse_xj } from "../../../core/data_formats/parsing/ninja";

View File

@ -9,8 +9,9 @@ import { TextureController } from "../controllers/TextureController";
export class TextureView extends ResizableView {
readonly element = div({ className: "viewer_TextureView" });
private readonly open_file_button = new FileButton("Open file...", {
private readonly open_file_button = new FileButton({
icon_left: Icon.File,
text: "Open file...",
accept: ".afs, .xvm",
});

View File

@ -8,14 +8,14 @@ exports[`Renders correctly without textures.: Should render a toolbar and a rend
class="core_ToolBar"
style="height: 33px;"
>
<label
class="core_FileButton core_Button"
<button
class="core_Button core_FileButton"
>
<span
class="core_FileButton_inner core_Button_inner"
class="core_Button_inner"
>
<span
class="core_FileButton_left core_Button_left"
class="core_Button_left"
>
<span>
<span
@ -29,12 +29,7 @@ exports[`Renders correctly without textures.: Should render a toolbar and a rend
Open file...
</span>
</span>
<input
accept=".afs, .xvm"
class="core_FileButton_input core_Button_inner"
type="file"
/>
</label>
</button>
</div>
<div
class="core_RendererWidget"

View File

@ -11,13 +11,13 @@ export class CharacterClassOptionsView extends ResizableView {
constructor(ctrl: CharacterClassOptionsController) {
super();
const section_id_select: Select<SectionId> = this.add(
const section_id_select: Select<SectionId | undefined> = this.add(
new Select({
class: "viewer_model_CharacterClassOptionsView_section_id",
label: "Section ID:",
items: SectionIds,
selected: ctrl.current_section_id,
to_label: section_id => SectionId[section_id],
to_label: section_id => (section_id == undefined ? "" : SectionId[section_id]),
enabled: ctrl.enabled,
}),
);

View File

@ -22,8 +22,9 @@ export class ModelToolBarView extends View {
constructor(ctrl: ModelToolBarController) {
super();
const open_file_button = new FileButton("Open file...", {
const open_file_button = new FileButton({
icon_left: Icon.File,
text: "Open file...",
accept: ".afs, .nj, .njm, .xj, .xvm",
});
const skeleton_checkbox = new CheckBox(false, { label: "Show skeleton" });

View File

@ -8,14 +8,14 @@ exports[`Renders correctly. 1`] = `
class="core_ToolBar"
style="height: 33px;"
>
<label
class="core_FileButton core_Button"
<button
class="core_Button core_FileButton"
>
<span
class="core_FileButton_inner core_Button_inner"
class="core_Button_inner"
>
<span
class="core_FileButton_left core_Button_left"
class="core_Button_left"
>
<span>
<span
@ -29,12 +29,7 @@ exports[`Renders correctly. 1`] = `
Open file...
</span>
</span>
<input
accept=".afs, .nj, .njm, .xj, .xvm"
class="core_FileButton_input core_Button_inner"
type="file"
/>
</label>
</button>
<div
class="core_ToolBar_group"
>

View File

@ -1 +1 @@
44
45