Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"style-loader": "4.0.0",
"tasuku": "^2.0.4",
"typescript": "5.9.3",
"uqr": "^0.1.3",
"victory": "37.3.6",
"webpack": "5.104.1",
"webpack-cli": "6.0.1",
Expand Down
3 changes: 0 additions & 3 deletions songs.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,6 @@
"jacket": {
"type": "string"
},
"author": {
"type": "string"
},
"bpm": {
"type": "string",
"description": "per-chart BPM range, if one applies"
Expand Down
26 changes: 26 additions & 0 deletions src/assets/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
},
"clearDrawings": "Clear Draws",
"clearDrawingsConfirm": "This will clear all songs drawn so far. Confirm?",
"dataImport": {
"title": "Data Import",
"smxEdits": "StepManiaX edits…",
"itgPack": "ITG / StepMania pack…"
},
"smxImport": {
"detected": "{count, plural, =0 {No codes detected yet} one {# unique code detected} other {# unique codes detected}}",
"lookingUp": "Looking them up now…",
"found": "{count, plural, one {Found info for # chart.} other {Found info for # charts.}}",
"importButton": "{count, plural, =0 {Import} one {Import # edit} other {Import # edits}}",
"imported": "{count, plural, one {Imported # edit chart.} other {Imported # edit charts.}}",
"skipped": "{count, plural, one { # edit was skipped because its song isn't in the StepManiaX data yet.} other { # edits were skipped because their songs aren't in the StepManiaX data yet.}}"
},
"credits": "Credits",
"help": "Help",
"toggleTheme": "Toggle Theme",
Expand Down Expand Up @@ -119,6 +132,19 @@
},
"clearDrawings": "全消去",
"clearDrawingsConfirm": "これまでに引いた譜面をすべて消去します。よろしいですか?",
"dataImport": {
"title": "データの取り込み",
"smxEdits": "StepManiaX エディット…",
"itgPack": "ITG / StepMania パック…"
},
"smxImport": {
"detected": "{count, plural, =0 {コードはまだ検出されていません} other {# 件のコードを検出しました}}",
"lookingUp": "照会中…",
"found": "{count, plural, other {# 件の譜面情報が見つかりました。}}",
"importButton": "{count, plural, =0 {取り込む} other {# 件のエディットを取り込む}}",
"imported": "{count, plural, other {# 件のエディット譜面を取り込みました。}}",
"skipped": "{count, plural, other { # 件のエディットは対象曲が StepManiaX データに未登録のためスキップされました。}}"
},
"credits": "クレジット",
"help": "使い方 (English)",
"toggleTheme": "テーマを切り替える",
Expand Down
149 changes: 104 additions & 45 deletions src/drop-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,48 @@ import {
Button,
Classes,
Dialog,
DialogBody,
DialogFooter,
FormGroup,
Switch,
} from "@blueprintjs/core";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { PackWithSongs } from "simfile-parser/browser";
import { useDrawState } from "./draw-state";
import { useImportUi } from "./state/import-ui";
import { getDataFileFromPack } from "./utils/itg-import";
import { pause } from "./utils/pause";
import { convertErrorToString } from "./utils/error-to-string";
import { Import } from "@blueprintjs/icons";
import { FolderOpen, Import } from "@blueprintjs/icons";
import { SmxEditImport } from "./smx-edit-import";

function loadParserModule() {
return import("simfile-parser/browser");
}

export function DropHandler() {
const [droppedFolder, setDroppedFolder] = useState<DataTransferItem | null>(
null,
);

const handleClose = useCallback(() => {
setDroppedFolder(null);
}, []);
const setPackSource = useImportUi((s) => s.setPackSource);

const handleDrop = useCallback(async (evt: DragEvent) => {
console.log("handle drop");
evt.preventDefault();
if (!evt.dataTransfer) {
return;
}
const handleDrop = useCallback(
async (evt: DragEvent) => {
console.log("handle drop");
evt.preventDefault();
if (!evt.dataTransfer) {
return;
}

if (evt.dataTransfer.items.length !== 1) {
console.error("too many items dropped");
return;
}
try {
setDroppedFolder(evt.dataTransfer.items[0]);
} catch (e) {
console.log(e);
}
}, []);
if (evt.dataTransfer.items.length !== 1) {
console.error("too many items dropped");
return;
}
try {
setPackSource(evt.dataTransfer.items[0]);
} catch (e) {
console.log(e);
}
},
[setPackSource],
);

const handleDragOver = useCallback(async (e: Event) => {
e.preventDefault();
Expand All @@ -61,35 +61,92 @@ export function DropHandler() {
});

return (
<ConfirmPackDialog
droppedFolder={droppedFolder}
onClose={handleClose}
onSave={handleClose}
/>
<>
<ItgInstructionsDialog />
<ConfirmPackDialog />
<SmxEditImport />
</>
);
}

interface DialogProps {
droppedFolder: DataTransferItem | null;
onSave(this: void): void;
onClose(this: void): void;
/**
* First step of the manual ITG import: explain what to select, then let the user
* pick a pack folder with a native directory picker. The chosen folder flows
* into the same confirm dialog used by drag/drop.
*/
function ItgInstructionsDialog() {
const isOpen = useImportUi((s) => s.itgInstructionsOpen);
const onClose = useImportUi((s) => s.closeItgInstructions);
const setPackSource = useImportUi((s) => s.setPackSource);
const inputRef = useRef<HTMLInputElement>(null);

const handleChange = useCallback(() => {
const input = inputRef.current;
if (input && input.files && input.files.length) {
setPackSource(input);
}
}, [setPackSource]);

return (
<Dialog
isOpen={isOpen}
title="Import an ITG/StepMania Pack"
icon={<FolderOpen />}
onClose={onClose}
>
<DialogBody>
<p>
Choose the folder of the pack you&apos;d like to import. This is the
folder that contains one subfolder per song (each with its own{" "}
<code>.sm</code> or <code>.ssc</code> simfile).
</p>
<p>
You can also drag and drop that folder anywhere onto this page to
start the same import.
</p>
<input
ref={inputRef}
type="file"
// @ts-expect-error non-standard attributes for directory selection
webkitdirectory=""
directory=""
style={{ display: "none" }}
onChange={handleChange}
/>
</DialogBody>
<DialogFooter
actions={
<>
<Button
intent="primary"
icon={<FolderOpen />}
onClick={() => inputRef.current?.click()}
>
Choose folder…
</Button>
<Button onClick={onClose}>Cancel</Button>
</>
}
/>
</Dialog>
);
}

function useDataParsing(
droppedFolder: DataTransferItem | null,
packSource: DataTransferItem | HTMLInputElement | null,
setTiered: (next: boolean) => void,
) {
const [parsedPack, setParsedPack] = useState<PackWithSongs | null>(null);
const [parseError, setParseError] = useState<string | null>(null);
useEffect(() => {
// oxlint-disable-next-line react-hooks-js/set-state-in-effect
setParseError(null);
if (!droppedFolder) {
if (!packSource) {
setParsedPack(null);
return;
}
loadParserModule()
.then(({ parsePack }) => parsePack(droppedFolder))
.then(({ parsePack }) => parsePack(packSource))
.then((pack) => {
setParsedPack(pack);
if (
Expand All @@ -107,19 +164,21 @@ function useDataParsing(
console.error(rejection);
setParseError(convertErrorToString(rejection));
});
}, [droppedFolder, setTiered]);
}, [packSource, setTiered]);
return {
parsedPack,
parseError,
};
}

function ConfirmPackDialog({ droppedFolder, onClose, onSave }: DialogProps) {
function ConfirmPackDialog() {
const packSource = useImportUi((s) => s.packSource);
const clearPackSource = useImportUi((s) => s.clearPackSource);
const [tiered, setTiered] = useState(false);
const [saving, setSaving] = useState(false);
const loadGameData = useDrawState((s) => s.addImportedData);

const { parsedPack, parseError } = useDataParsing(droppedFolder, setTiered);
const { parsedPack, parseError } = useDataParsing(packSource, setTiered);
const derivedData = useMemo(() => {
if (!parsedPack) {
return;
Expand All @@ -135,8 +194,8 @@ function ConfirmPackDialog({ droppedFolder, onClose, onSave }: DialogProps) {
loadGameData(parsedPack.name, derivedData);
await pause(500);
setSaving(false);
onSave();
}, [parsedPack, derivedData, loadGameData, onSave]);
clearPackSource();
}, [parsedPack, derivedData, loadGameData, clearPackSource]);

const maybeSkeleton = derivedData ? "" : Classes.SKELETON;

Expand Down Expand Up @@ -180,9 +239,9 @@ function ConfirmPackDialog({ droppedFolder, onClose, onSave }: DialogProps) {

return (
<Dialog
isOpen={!!droppedFolder}
isOpen={!!packSource}
title="Local Data Import"
onClose={onClose}
onClose={clearPackSource}
>
<div style={{ padding: "10px" }}>{body}</div>
<DialogFooter
Expand All @@ -197,7 +256,7 @@ function ConfirmPackDialog({ droppedFolder, onClose, onSave }: DialogProps) {
>
Import
</Button>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={clearPackSource}>Cancel</Button>
</>
}
/>
Expand Down
28 changes: 27 additions & 1 deletion src/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ import {
Navbar,
Popover,
} from "@blueprintjs/core";
import { Trash, InfoSign, Menu as MenuIcon, Help } from "@blueprintjs/icons";
import {
Trash,
InfoSign,
Menu as MenuIcon,
Help,
Database,
Import,
FolderOpen,
} from "@blueprintjs/icons";
import { useState } from "react";
import { About } from "./about";
import { HeaderControls } from "./controls";
Expand All @@ -16,11 +24,14 @@ import { LastUpdate } from "./last-update";
import { ThemeToggle } from "./theme-toggle";
import { DataLoadingSpinner, VersionSelect } from "./version-select";
import { useDrawState } from "./draw-state";
import { useImportUi } from "./state/import-ui";

export function Header() {
const [aboutOpen, setAboutOpen] = useState(false);
const clearDrawings = useDrawState((d) => d.clearDrawings);
const haveDrawings = useDrawState((d) => !!d.drawings.length);
const openItgInstructions = useImportUi((s) => s.openItgInstructions);
const openSmxEdits = useImportUi((s) => s.openSmxEdits);
const { t } = useIntl();

const menu = (
Expand All @@ -31,6 +42,21 @@ export function Header() {
text={t("clearDrawings")}
disabled={!haveDrawings}
/>
<MenuItem
icon={<Database />}
text={t("dataImport.title", undefined, "Data Import")}
>
<MenuItem
icon={<Import />}
onClick={openSmxEdits}
text={t("dataImport.smxEdits", undefined, "StepManiaX edits…")}
/>
<MenuItem
icon={<FolderOpen />}
onClick={openItgInstructions}
text={t("dataImport.itgPack", undefined, "ITG / StepMania pack…")}
/>
</MenuItem>
<MenuItem
icon={<InfoSign />}
onClick={() => setAboutOpen(true)}
Expand Down
1 change: 0 additions & 1 deletion src/models/SongData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ export interface Chart {
drawGroup?: number;
maxScore?: number;
jacket?: string;
author?: string;
/**
* per-chart BPM range, if one applies
*/
Expand Down
Loading