Skip to content

Commit

Permalink
feat: add SVG snapshot saving
Browse files Browse the repository at this point in the history
closes #150
  • Loading branch information
Julian Mills committed Dec 8, 2023
1 parent fcb4a4e commit c9c0df7
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 62 deletions.
2 changes: 1 addition & 1 deletion src/adapter/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface SurrealistAdapter {
title: string,
defaultPath: string,
filters: any,
content: () => Result<string>
content: () => Result<string | Blob | null>
): Promise<boolean>;

/**
Expand Down
13 changes: 11 additions & 2 deletions src/adapter/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@ export class BrowserAdapter implements SurrealistAdapter {
_title: string,
defaultPath: string,
_filters: any,
content: () => Result<string>
content: () => Result<string | Blob | null>
): Promise<boolean> {
const file = new File([await content()], '', { type: 'text/plain' });
const result = await content();

if (!result) {
return false;
}

const file = (typeof result === 'string')
? new File([result], '', { type: 'text/plain' })
: result;

const url = window.URL.createObjectURL(file);
const el = document.createElement('a');

Expand Down
16 changes: 13 additions & 3 deletions src/adapter/desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { showNotification } from "@mantine/notifications";
import { store } from "~/store";
import { SurrealistAdapter } from "./base";
import { printLog } from "~/util/helpers";
import { readTextFile, writeTextFile } from "@tauri-apps/api/fs";
import { readTextFile, writeBinaryFile, writeTextFile } from "@tauri-apps/api/fs";
import { Result } from "~/typings/utilities";
import { confirmServing, pushConsoleLine, stopServing } from "~/stores/database";

Expand Down Expand Up @@ -88,15 +88,25 @@ export class DesktopAdapter implements SurrealistAdapter {
title: string,
defaultPath: string,
filters: any,
content: () => Result<string>
content: () => Result<string | Blob | null>
): Promise<boolean> {
const filePath = await save({ title, defaultPath, filters });

if (!filePath) {
return false;
}

await writeTextFile(filePath, await content());
const result = await content();

if (!result) {
return false;
}

if (typeof result === "string") {
await writeTextFile(filePath, result);
} else {
await writeBinaryFile(filePath, await result.arrayBuffer());
}

return true;
}
Expand Down
14 changes: 13 additions & 1 deletion src/views/designer/TableGraphPane/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import dagere from "dagre";
import { TableNode } from "~/views/designer/TableGraphPane/nodes/TableNode";
import { EdgeNode } from "./nodes/EdgeNode";
import { DesignerNodeMode, TableDefinition } from "~/types";
import { extractEdgeRecords } from "~/util/schema";
import { Edge, Node, Position } from "reactflow";
import dagere from "dagre";
import { toBlob, toSvg } from "html-to-image";

type HeightMap = Record<DesignerNodeMode, (t: TableDefinition, e: boolean) => number>;

Expand Down Expand Up @@ -137,3 +138,14 @@ export function buildTableGraph(

return [nodes, edges];
}

export async function createSnapshot(el: HTMLElement, type: 'png' | 'svg') {
if (type == 'png') {
return toBlob(el, { cacheBust: true });
} else {
const dataUrl = await toSvg(el, { cacheBust: true });
const res = await fetch(dataUrl);

return await res.blob();
}
}
131 changes: 76 additions & 55 deletions src/views/designer/TableGraphPane/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { ActionIcon, Box, Button, Center, Group, Kbd, LoadingOverlay, Modal, Paper, Popover, Stack, Text, Title } from "@mantine/core";
import { mdiAdjust, mdiCog, mdiDownload, mdiHelpCircle, mdiPlus } from "@mdi/js";
import { mdiAdjust, mdiCog, mdiDownload, mdiHelpCircle, mdiImageOutline, mdiPlus, mdiXml } from "@mdi/js";
import { ElementRef, useEffect, useRef, useState } from "react";
import { Icon } from "~/components/Icon";
import { Panel } from "~/components/Panel";
import { DesignerLayoutMode, DesignerNodeMode, TableDefinition } from "~/types";
import { toBlob } from "html-to-image";
import { ReactFlow, useEdgesState, useNodesState } from "reactflow";
import { NODE_TYPES, buildTableGraph as buildTableDiagram } from "./helpers";
import { NODE_TYPES, buildTableGraph as buildTableDiagram, createSnapshot } from "./helpers";
import { useStable } from "~/hooks/stable";
import { save } from "@tauri-apps/api/dialog";
import { writeBinaryFile } from "@tauri-apps/api/fs";
import { useLater } from "~/hooks/later";
import { useIsLight } from "~/hooks/theme";
import { showNotification } from "@mantine/notifications";
import { useIsConnected } from "~/hooks/connection";
import { TableCreator } from "~/components/TableCreator";
import { ModalTitle } from "~/components/ModalTitle";
Expand All @@ -24,6 +19,8 @@ import { TableGrid } from "./grid";
import { RadioSelect } from "~/components/RadioSelect";
import { useToggleList } from "~/hooks/toggle";
import { updateSession } from "~/stores/config";
import { adapter } from "~/adapter";
import { showNotification } from "@mantine/notifications";

interface HelpTitleProps {
isLight: boolean;
Expand Down Expand Up @@ -51,6 +48,7 @@ export function TableGraphPane(props: TableGraphPaneProps) {
const [isRendering, setIsRendering] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [showExporter, setShowExporter] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const ref = useRef<ElementRef<"div">>(null);
const isOnline = useIsConnected();
Expand All @@ -72,43 +70,27 @@ export function TableGraphPane(props: TableGraphPaneProps) {
setEdges(edges);
}, [props.tables, props.active, nodeMode, expanded]);

const createSnapshot = useStable(async (filePath: string) => {
const contents = await toBlob(ref.current!, { cacheBust: true });

if (!contents) {
return;
}

await writeBinaryFile(filePath, await contents.arrayBuffer());

setIsRendering(false);

showNotification({
message: "Snapshot saved to disk",
const saveImage = useStable(async (type: 'png' | 'svg') => {
const isSuccess = await adapter.saveFile("Save snapshot", `snapshot.${type}`, [
{
name: "Image",
extensions: [type],
},
], async () => {
setIsRendering(true);

try {
return await createSnapshot(ref.current!, type);
} finally {
setIsRendering(false);
}
});
});

const scheduleSnapshot = useLater(createSnapshot);

// TODO Move download logic to adapter
const saveImage = useStable(async () => {
const filePath = await save({
title: "Save snapshot",
defaultPath: "snapshot.png",
filters: [
{
name: "Image",
extensions: ["png", "jpg", "jpeg"],
},
],
});

if (!filePath) {
return;

if (isSuccess) {
showNotification({
message: "Snapshot saved to disk"
});
}

setIsRendering(true);
scheduleSnapshot(filePath);
});

const openHelp = useStable(() => {
Expand All @@ -134,6 +116,10 @@ export function TableGraphPane(props: TableGraphPaneProps) {
setShowConfig(v => !v);
});

const toggleExporter = useStable(() => {
setShowExporter(v => !v);
});

const setNodeMode = useStable((mode: string) => {
store.dispatch(updateSession({
id: activeSession?.id,
Expand All @@ -157,6 +143,54 @@ export function TableGraphPane(props: TableGraphPaneProps) {
<ActionIcon title="Create table..." onClick={openCreator}>
<Icon color="light.4" path={mdiPlus} />
</ActionIcon>
<Popover
opened={showExporter}
onChange={setShowExporter}
position="bottom-end"
offset={{ crossAxis: -4, mainAxis: 8 }}
withArrow
withinPortal
shadow={`0 8px 25px rgba(0, 0, 0, ${isLight ? 0.2 : 0.75})`}
>
<Popover.Target>
<ActionIcon
title="Export Graph"
onClick={toggleExporter}
>
<Icon color="light.4" path={mdiDownload} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown onMouseLeave={toggleExporter} p="xs">
<Stack pb={4}>
<Button
color="surreal"
variant="subtle"
fullWidth
size="xs"
onClick={() => saveImage('png')}
>
Save as PNG
<Icon
right
path={mdiImageOutline}
/>
</Button>
<Button
color="surreal"
variant="subtle"
fullWidth
size="xs"
onClick={() => saveImage('svg')}
>
Save as SVG
<Icon
right
path={mdiXml}
/>
</Button>
</Stack>
</Popover.Dropdown>
</Popover>
<Popover
opened={showConfig}
onChange={setShowConfig}
Expand Down Expand Up @@ -192,19 +226,6 @@ export function TableGraphPane(props: TableGraphPaneProps) {
value={nodeMode}
onChange={setNodeMode}
/>

<Button
color="surreal"
fullWidth
size="xs"
onClick={saveImage}
>
Save snapshot
<Icon
right
path={mdiDownload}
/>
</Button>
</Stack>
</Popover.Dropdown>
</Popover>
Expand Down

0 comments on commit c9c0df7

Please sign in to comment.