diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 771f47a..5a30a55 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -25,6 +25,7 @@
"clsx": "catalog:",
"common": "workspace:^",
"effect": "catalog:",
+ "jszip": "catalog:",
"lucide-react": "catalog:",
"opensheetmusicdisplay": "catalog:",
"react": "catalog:",
diff --git a/packages/frontend/src/app.tsx b/packages/frontend/src/app.tsx
index 551477a..cab2589 100644
--- a/packages/frontend/src/app.tsx
+++ b/packages/frontend/src/app.tsx
@@ -67,13 +67,21 @@ const router = createBrowserRouter([
path: "/login",
Component: Login,
},
-]);
+], {
+ future: {
+ v7_fetcherPersist: true,
+ v7_normalizeFormMethod: true,
+ v7_partialHydration: true,
+ v7_relativeSplatPath: true,
+ v7_skipActionErrorRevalidation: true,
+ },
+});
const rootElement = document.getElementById("root") as HTMLDivElement;
const root = createRoot(rootElement);
root.render(
-
+
,
);
diff --git a/packages/frontend/src/routes/Attachment.tsx b/packages/frontend/src/routes/Attachment.tsx
index 92e9808..2557636 100644
--- a/packages/frontend/src/routes/Attachment.tsx
+++ b/packages/frontend/src/routes/Attachment.tsx
@@ -1,8 +1,9 @@
import { client } from "@/client";
import { useLoading } from "@/hooks/useLoading.ts";
import { AttachmentId, PieceId } from "common";
+import JSZip from "jszip";
import { OpenSheetMusicDisplay } from "opensheetmusicdisplay";
-import { useEffect, useRef } from "react";
+import { useCallback, useEffect, useRef } from "react";
import { useParams } from "react-router-dom";
export default function Attachment() {
@@ -14,33 +15,81 @@ export default function Attachment() {
const { isLoading, error, data } = useLoading(() => client.piece({ pieceId }).attachment({ attachmentId }).get(), [pieceId, attachmentId]);
const containerRef = useRef(null);
+ const renderFn = useRef void)>(null);
+
+ const load = useCallback(async () => {
- useEffect(() => {
if (isLoading || error !== null) return;
- const url = URL.createObjectURL(data);
+ let musixXmlBlob: Blob = data;
- const render = () => osmd.render();
+ /* If the file is the compressed .mxl file, we do the uncompression
+ * ourselves, because apparently OpenSheetMusicDisplay is incapable.
+ */
+ if (data.type === "application/vnd.recordare.musicxml") {
+ const zip = new JSZip();
+ await zip.loadAsync(data);
+
+ const containerFile = zip.file("META-INF/container.xml");
+ if (containerFile === null) {
+ console.error("Missing META-INF/container.xml in the .mxl file");
+ return;
+ }
+
+ const containerText = await containerFile.async("text");
+ const containerXml = new DOMParser().parseFromString(containerText, "application/xml");
+
+ const rootFile = containerXml.querySelector("rootfile[media-type=\"application/vnd.recordare.musicxml+xml\"], rootfile:not([media-type])");
+ if (rootFile === null) {
+ console.error("Missing root MusicXML file in META-INF/container.xml");
+ return;
+ }
+
+ const fullPath = rootFile.getAttribute("full-path");
+ if (fullPath === null) {
+ console.error("Missing full-path attribute in rootfile element");
+ return;
+ }
+
+ const musicXmlFile = zip.file(fullPath);
+ if (musicXmlFile === null) {
+ console.error(`Missing ${fullPath} file in the .mxl archive`);
+ return;
+ }
+
+ musixXmlBlob = await musicXmlFile.async("blob");
+ }
+
+ const musicXml = await musixXmlBlob.text();
const osmd = new OpenSheetMusicDisplay(containerRef.current!, {
autoResize: false,
drawTitle: false,
drawComposer: false,
drawMeasureNumbers: true,
- drawMeasureNumbersOnlyAtSystemStart: true,
- //measureNumberInterval: 5,
- //renderSingleHorizontalStaffline: true,
+ drawMeasureNumbersOnlyAtSystemStart: false,
+ measureNumberInterval: 1,
+ renderSingleHorizontalStaffline: true,
});
- osmd.load(url).then(render, (error) => console.error(error));
+ const render = () => osmd.render();
+
+ await osmd.load(musicXml);
+ render();
+
+ renderFn.current = render;
+ }, [data, error, isLoading]);
+
+ useEffect(() => void load(), [load]);
+
+ useEffect(() => {
+ const render = () => renderFn.current?.();
window.addEventListener("resize", render);
-
return () => {
- URL.revokeObjectURL(url);
window.removeEventListener("resize", render);
};
- }, [data, error, isLoading]);
+ }, []);
if (isLoading) {
return (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 76fe5cc..bee8091 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -66,6 +66,9 @@ catalogs:
eslint-plugin-react-hooks:
specifier: ^5.1.0
version: 5.1.0
+ jszip:
+ specifier: ^3.10.1
+ version: 3.10.1
kysely:
specifier: ^0.27.4
version: 0.27.4
@@ -192,6 +195,9 @@ importers:
effect:
specifier: 'catalog:'
version: 3.11.4
+ jszip:
+ specifier: 'catalog:'
+ version: 3.10.1
lucide-react:
specifier: 'catalog:'
version: 0.462.0(react@18.3.1)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 0808841..251b700 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -22,6 +22,7 @@ catalog:
effect: '^3.11.4'
elysia: '^1.1.25'
eslint-plugin-react-hooks: '^5.1.0'
+ jszip: '^3.10.1'
kysely: '^0.27.4'
kysely-bun-sqlite: '^0.3.2'
lucide-react: '^0.462.0'