Embedded, browser-provided PDF viewer

This commit is contained in:
2025-10-16 00:36:14 +02:00
parent 350ebc90c8
commit 3483609f19
2 changed files with 85 additions and 47 deletions

View File

@@ -4,7 +4,7 @@ import { AttachmentId } from "common";
import { Match } from "effect";
import JSZip from "jszip";
import { OpenSheetMusicDisplay } from "opensheetmusicdisplay";
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
export default function Attachment() {
@@ -14,19 +14,62 @@ export default function Attachment() {
const { isLoading, error, data } = useLoading(client.getAttachment(attachmentId), [attachmentId]);
if (isLoading) {
return (
<div className="w-full h-full overflow-hidden flex items-center justify-center">
<div>Ładowanie</div>
</div>
);
}
if (error !== null) {
return (
<div className="w-full h-full overflow-hidden flex items-center justify-center">
<div>
Wystąpił błąd: {Match.value(error).pipe(
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
Match.tag("NotFound", () => "Załącznik nie istnieje"),
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
Match.tag("Unauthorized", () => "Nie posiadasz uprawnień"),
Match.exhaustive,
)}
</div>
</div>
);
}
if (data.mediaType === "application/vnd.recordare.musicxml" || data.mediaType === "application/vnd.recordare.musicxml+xml") {
return <MusicXmlAttachment data={data.data} mediaType={data.mediaType} />;
} else if (data.mediaType === "application/pdf") {
return <PdfAttachment data={data.data} filename={data.filename} mediaType={data.mediaType} />;
} else {
return null;
}
};
namespace MusicXmlAttachment {
export interface Props {
readonly data: Uint8Array<ArrayBuffer>;
readonly mediaType:
| "application/vnd.recordare.musicxml"
| "application/vnd.recordare.musicxml+xml"
;
}
}
function MusicXmlAttachment({ data, mediaType }: MusicXmlAttachment.Props) {
const containerRef = useRef<HTMLDivElement>(null);
const renderFn = useRef<null | (() => void)>(null);
const load = useCallback(async () => {
if (isLoading || error !== null) return;
let musixXmlData: Uint8Array = data.data;
let musixXmlData: Uint8Array = data;
/* If the file is the compressed .mxl file, we do the uncompression
* ourselves, because apparently OpenSheetMusicDisplay is incapable.
*/
if (data.mediaType === "application/vnd.recordare.musicxml") {
if (mediaType === "application/vnd.recordare.musicxml") {
const zip = new JSZip();
await zip.loadAsync(musixXmlData);
@@ -78,7 +121,7 @@ export default function Attachment() {
render();
renderFn.current = render;
}, [data, error, isLoading]);
}, [data]);
useEffect(() => void load(), [load]);
@@ -91,29 +134,39 @@ export default function Attachment() {
};
}, []);
if (isLoading) {
return (
<div className="w-full h-full overflow-hidden flex items-center justify-center">
<div>Ładowanie</div>
</div>
);
}
if (error !== null) {
return (
<div className="w-full h-full overflow-hidden flex items-center justify-center">
<div>
Wystąpił błąd: {Match.value(error).pipe(
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
Match.tag("NotFound", () => "Załącznik nie istnieje"),
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
Match.tag("Unauthorized", () => "Nie posiadasz uprawnień"),
Match.exhaustive,
)}
</div>
</div>
);
}
return <div ref={containerRef} className="w-full h-full overflow-scroll" />;
};
}
namespace PdfAttachment {
export interface Props {
readonly data: Uint8Array<ArrayBuffer>;
readonly filename: string;
readonly mediaType: "application/pdf";
}
}
function PdfAttachment({
data,
filename,
mediaType,
}: PdfAttachment.Props) {
const [url, setUrl] = useState<string>("");
useLayoutEffect(() => {
const file = new File([data], filename, { type: mediaType });
const url = URL.createObjectURL(file);
setUrl(url);
return () => { URL.revokeObjectURL(url); };
}, [data, mediaType]);
return url && (
<object
type={mediaType}
data={url}
className="w-full h-full"
/>
);
}

View File

@@ -233,21 +233,6 @@ function AttachmentRow(props: AttachmentRow.Props) {
URL.revokeObjectURL(url);
}).pipe(Effect.runPromise);
const open = (event: MouseEvent<HTMLAnchorElement>) => Effect.gen(function* () {
if (props.attachment.mediaType !== "application/pdf") {
return;
}
event.preventDefault();
const { data, mediaType } = yield* client.getAttachment(props.attachment.attachmentId);
const blob = new Blob([data], { type: mediaType });
const url = URL.createObjectURL(blob);
window.open(url, "_target");
URL.revokeObjectURL(url);
}).pipe(Effect.runPromise);
const doDelete = () => Effect.gen(function* () {
yield* client.deleteAttachment(props.attachment.attachmentId);
yield* pieceCache.update(props.attachment.pieceId, mapProp("attachments", Array.filter<Attachment>((a) => a.attachmentId !== props.attachment.attachmentId)));
@@ -280,7 +265,7 @@ function AttachmentRow(props: AttachmentRow.Props) {
{props.attachment.mediaType === "application/vnd.recordare.musicxml"
|| props.attachment.mediaType === "application/vnd.recordare.musicxml+xml"
|| props.attachment.mediaType === "application/pdf" ? (
<Link className="underline" to={`attachment/${props.attachment.attachmentId}`} onClick={open}>
<Link className="underline" to={`attachment/${props.attachment.attachmentId}`}>
{props.attachment.filename}
</Link>
) : (