Embedded, browser-provided PDF viewer
This commit is contained in:
@@ -4,7 +4,7 @@ import { AttachmentId } from "common";
|
|||||||
import { Match } from "effect";
|
import { Match } from "effect";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { OpenSheetMusicDisplay } from "opensheetmusicdisplay";
|
import { OpenSheetMusicDisplay } from "opensheetmusicdisplay";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function Attachment() {
|
export default function Attachment() {
|
||||||
@@ -14,19 +14,62 @@ export default function Attachment() {
|
|||||||
|
|
||||||
const { isLoading, error, data } = useLoading(client.getAttachment(attachmentId), [attachmentId]);
|
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 containerRef = useRef<HTMLDivElement>(null);
|
||||||
const renderFn = useRef<null | (() => void)>(null);
|
const renderFn = useRef<null | (() => void)>(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
|
|
||||||
if (isLoading || error !== null) return;
|
let musixXmlData: Uint8Array = data;
|
||||||
|
|
||||||
let musixXmlData: Uint8Array = data.data;
|
|
||||||
|
|
||||||
/* If the file is the compressed .mxl file, we do the uncompression
|
/* If the file is the compressed .mxl file, we do the uncompression
|
||||||
* ourselves, because apparently OpenSheetMusicDisplay is incapable.
|
* ourselves, because apparently OpenSheetMusicDisplay is incapable.
|
||||||
*/
|
*/
|
||||||
if (data.mediaType === "application/vnd.recordare.musicxml") {
|
if (mediaType === "application/vnd.recordare.musicxml") {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
await zip.loadAsync(musixXmlData);
|
await zip.loadAsync(musixXmlData);
|
||||||
|
|
||||||
@@ -78,7 +121,7 @@ export default function Attachment() {
|
|||||||
render();
|
render();
|
||||||
|
|
||||||
renderFn.current = render;
|
renderFn.current = render;
|
||||||
}, [data, error, isLoading]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => void load(), [load]);
|
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" />;
|
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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -233,21 +233,6 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}).pipe(Effect.runPromise);
|
}).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* () {
|
const doDelete = () => Effect.gen(function* () {
|
||||||
yield* client.deleteAttachment(props.attachment.attachmentId);
|
yield* client.deleteAttachment(props.attachment.attachmentId);
|
||||||
yield* pieceCache.update(props.attachment.pieceId, mapProp("attachments", Array.filter<Attachment>((a) => a.attachmentId !== 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"
|
||||||
|| props.attachment.mediaType === "application/vnd.recordare.musicxml+xml"
|
|| props.attachment.mediaType === "application/vnd.recordare.musicxml+xml"
|
||||||
|| props.attachment.mediaType === "application/pdf" ? (
|
|| 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}
|
{props.attachment.filename}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user