Skip to content

Commit

Permalink
Add export function
Browse files Browse the repository at this point in the history
  • Loading branch information
ducquando committed Apr 17, 2024
1 parent a51f3a3 commit 699302a
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 132 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeslide.net",
"version": "0.3.2",
"version": "0.3.3",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.8.1",
Expand Down
1 change: 1 addition & 0 deletions src/wireframes/components/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export * from './use-clipboard';
export * from './use-grouping';
export * from './use-history';
export * from './use-loading';
export * from './use-server';
export * from './use-remove';
155 changes: 155 additions & 0 deletions src/wireframes/components/actions/use-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Diagram, changeFrames, compileSlides, generatePdf, getEditor, getFilteredDiagrams, parseFrames, useStore } from "@app/wireframes/model";
import { AbstractControl } from "@app/wireframes/shapes/utils/abstract-control";
import { getPlugin } from "@app/wireframes/shapes/utils/abstract-plugin";
import * as svg from '@svgdotjs/svg.js';
import { Color } from "@app/core/utils/color";
import { shapes } from "@app/const";
import { MessageInstance } from "antd/es/message/interface";
import saveAs from "file-saver";
import { useDispatch } from "react-redux";

export function useServer() {
const dispatch = useDispatch();
const diagrams = useStore(getFilteredDiagrams);
const editor = useStore(getEditor);

const getItem = (diagram: Diagram, str: string) => {
// Split text using `=` symbol, ignoring those inside curly brackets
const regex = /=(?![^{]*})/;
const [id, json] = str.split(regex);

// Get item
let item = diagram.items.get(id);
if (!item || !json) return { item: item, id: id };

// Parse str -> json
let corrected = json.replace(/'/g, '"');
let jsonObj: {[index: string]: string} = JSON.parse(corrected);

// Modify appearance if there are valid specifications
// e.g. Shape1 = {'TEXT': 'Hello, world!'}
const allKeys = Object.values(shapes.key);
for (let [key, value] of Object.entries(jsonObj)) {
if (!allKeys.includes(key)) continue; // Safe-check

if (key.endsWith('COLOR')) {
const color = Color.fromValue(value).toNumber();
item = item.setAppearance(key, color);
} else {
item = item.setAppearance(key, value);
}
}

return { item: item, id: id };
}

const getSlides = () => {
let frame3D: string[][][] = new Array(diagrams.length);

// Get frames
diagrams.map((diagram, i) => {
const frames = diagram.frames ?? [];
frame3D[i] = [];

frames.map((frame, j) => {
const usedIDs: string[] = [];
frame3D[i][j] = [];

for (let k = frame.length; k > 0; k--) {
const { item, id } = getItem(diagram, frame[k - 1]);
if (!item || usedIDs.includes(id)) continue;
usedIDs.push(id);

// Get svg
const svgControl = new AbstractControl(getPlugin(item.renderer));
const svgElement: svg.Element = svgControl.render(item, undefined);
const svgCode = svgElement.node.outerHTML;

// Push object to the head of parent map
frame3D[i][j] = [svgCode, ...frame3D[i][j]];
}
})
})

// Reshape from 3D to 2D
let frame2D = [];
for (let row of frame3D) for (let e of row) frame2D.push(e);

return {
fileName: editor.id,
title: editor.name,
backgroundColor: editor.color.toString(),
size: [editor.size.x, editor.size.y],
config: editor.revealConfig,
frame: frame2D,
};
}

const fetchParser = async () => {
for (let diagram of diagrams) {
const script = diagram.script;
if (!script) continue;

const frames = await parseFrames(script);
dispatch(changeFrames(diagram.id, frames));
}
}

const fetchCompiler = async () => {
const { fileName, title, size, backgroundColor, config, frame } = getSlides();

const linkPresentation = await compileSlides(fileName, title, size, backgroundColor, config, frame);

return linkPresentation;
}

const fetchApi = () => {
// Fetch frames from parser
fetchParser();

// Fetch presentation from compiler
const links = fetchCompiler();
return links;
}

const fetchPdf = async (messageApi: MessageInstance, messageKey: string) => {
// Start compiling
messageApi.open({ key: messageKey, type: 'loading', content: 'Exporting presentation...' });

try {
const { linkPdf } = await fetchApi();
const blob = await generatePdf(linkPdf);
saveAs(blob, linkPdf);
} catch (err) {
messageApi.error(`${err}`);
} finally {
messageApi.open({
key: messageKey,
type: 'success',
content: `Export completed. Your presentation will be downloaded.`,
duration: 1,
});
}
}

const fetchSlide = async (messageApi: MessageInstance, messageKey: string) => {
messageApi.open({ key: messageKey, type: 'loading', content: 'Preparing presentation...' });

try {
const { linkSlide } = await fetchApi();
window.open(linkSlide, '_blank');
} catch (err) {
messageApi.error(`${err}`);
} finally {
messageApi.open({
key: messageKey,
type: 'success',
content: `Preparing completed. Your presentation will be opened in a new tab.`,
duration: 1,
});
}
}

return { slide: fetchSlide, pdf: fetchPdf };
}

17 changes: 15 additions & 2 deletions src/wireframes/components/headers/FileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
*/

import { changeName, getEditor, useStore } from '@app/wireframes/model';
import { Button, Dropdown, Form, Input } from 'antd';
import { Button, Dropdown, Form, Input, message } from 'antd';
import { useEffect, useState } from 'react';
import { useLoading } from '../actions';
import { useLoading, useServer } from '../actions';
import { texts } from '@app/const/texts';
import { useDispatch } from 'react-redux';
import { FormModal, SettingModal } from '../modal';
Expand All @@ -19,9 +19,12 @@ import { MenuIcon } from '@app/style/icomoon/icomoon_icon';
export const FileHeader = () => {
const dispatch = useDispatch();
const forLoading = useLoading();
const forServer = useServer();
const editor = useStore(getEditor);
const [messageApi, contextHolder] = message.useMessage();
const [isRename, setIsRename] = useState(false);
const [isSettings, setIsSettings] = useState(false);
const messageKey = 'GENERATE';

// Get editor's name
let name = editor.name;
Expand Down Expand Up @@ -81,6 +84,13 @@ export const FileHeader = () => {
className: 'loading-action-item',
disabled: forLoading.downloadDiagram.disabled,
},
{
key: texts.common.saveDiagramToFileTooltip,
label: texts.common.saveDiagramToFileTooltip,
icon: <MenuIcon icon='icon-save' />,
className: 'loading-action-item',
disabled: forLoading.downloadDiagram.disabled,
},
];

const menuEvt: MenuProps['onClick'] = ({key}) => {
Expand All @@ -94,11 +104,14 @@ export const FileHeader = () => {
dispatch(forLoading.openDiagramAction.onAction);
} else if (key == forLoading.downloadDiagram.label) {
dispatch(forLoading.downloadDiagram.onAction);
} else if (texts.common.saveDiagramToFileTooltip) {
dispatch(forServer.pdf(messageApi, messageKey));
}
}

return (
<>
{contextHolder}
<Dropdown
className='loading-action-button'
menu={{ items: menu, onClick: menuEvt }}
Expand Down
138 changes: 11 additions & 127 deletions src/wireframes/components/headers/PresentHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,74 +9,30 @@
import { FundProjectionScreenOutlined } from "@ant-design/icons";
import { Button, message } from "antd";
import * as React from "react";
import { getFilteredDiagrams, getEditor, useStore, Diagram, compileSlides, EditorState, parseFrames, changeFrames } from "@app/wireframes/model";
import { AbstractControl } from "@app/wireframes/shapes/utils/abstract-control";
import * as svg from '@svgdotjs/svg.js';
import { getPlugin } from "@app/wireframes/shapes/utils/abstract-plugin";
import { Color } from "@app/core/utils/color";
import { shapes } from "@app/const";
import { useState } from "react";
import { useServer } from "../actions";
import { useDispatch } from "react-redux";

export const PresentHeader = React.memo(() => {
const dispatch = useDispatch();
const diagrams = useStore(getFilteredDiagrams);
const editor = useStore(getEditor);
const forServer = useServer();
const [messageApi, contextHolder] = message.useMessage();
const [loading, setLoading] = useState<boolean>(false);

const messageKey = 'PRESENT';

const fetchParser = async () => {
for (let diagram of diagrams) {
const script = diagram.script;
if (!script) continue;

const frames = await parseFrames(script);
dispatch(changeFrames(diagram.id, frames));
}
}

const fetchCompiler = async () => {
const { fileName, title, size, backgroundColor, config, frame } = getSlides(diagrams, editor);

const linkPresentation = await compileSlides(fileName, title, size, backgroundColor, config, frame);

return linkPresentation;
}

const fetchApi = async () => {
// Start compiling
setLoading(true);
messageApi.open({ key: messageKey, type: 'loading', content: 'Preparing presentation...' });

try {
// Fetch frames from parser
fetchParser();

// Fetch presentation from compiler
const link = fetchCompiler();
window.open(await link, '_blank');
} catch (err) {
messageApi.error(`${err}`);
} finally {
messageApi.open({
key: messageKey,
type: 'success',
content: `Preparing completed. Your presentation will be opened in a new tab.`,
duration: 1,
});
}

setLoading(false);
}

return (
<>
{contextHolder}
<Button
icon={<FundProjectionScreenOutlined />}
onClick={fetchApi}
onClick={() => {
setLoading(true);
try {
dispatch(forServer.slide(messageApi, messageKey));
} finally {
setLoading(false)
}
}}
className="header-cta-right"
type="text" shape='round'
loading={loading}
Expand All @@ -85,76 +41,4 @@ export const PresentHeader = React.memo(() => {
</Button>
</>
)
});

const getItem = (diagram: Diagram, str: string) => {
// Split text using `=` symbol, ignoring those inside curly brackets
const regex = /=(?![^{]*})/;
const [id, json] = str.split(regex);

// Get item
let item = diagram.items.get(id);
if (!item || !json) return { item: item, id: id };

// Parse str -> json
let corrected = json.replace(/'/g, '"');
let jsonObj: {[index: string]: string} = JSON.parse(corrected);

// Modify appearance if there are valid specifications
// e.g. Shape1 = {'TEXT': 'Hello, world!'}
const allKeys = Object.values(shapes.key);
for (let [key, value] of Object.entries(jsonObj)) {
if (!allKeys.includes(key)) continue; // Safe-check

if (key.endsWith('COLOR')) {
const color = Color.fromValue(value).toNumber();
item = item.setAppearance(key, color);
} else {
item = item.setAppearance(key, value);
}
}

return { item: item, id: id };
}

const getSlides = (diagrams: Diagram[], editor: EditorState) => {
let frame3D: string[][][] = new Array(diagrams.length);

// Get frames
diagrams.map((diagram, i) => {
const frames = diagram.frames ?? [];
frame3D[i] = [];

frames.map((frame, j) => {
const usedIDs: string[] = [];
frame3D[i][j] = [];

for (let k = frame.length; k > 0; k--) {
const { item, id } = getItem(diagram, frame[k - 1]);
if (!item || usedIDs.includes(id)) continue;
usedIDs.push(id);

// Get svg
const svgControl = new AbstractControl(getPlugin(item.renderer));
const svgElement: svg.Element = svgControl.render(item, undefined);
const svgCode = svgElement.node.outerHTML;

// Push object to the head of parent map
frame3D[i][j] = [svgCode, ...frame3D[i][j]];
}
})
})

// Reshape from 3D to 2D
let frame2D = [];
for (let row of frame3D) for (let e of row) frame2D.push(e);

return {
fileName: editor.id,
title: editor.name,
backgroundColor: editor.color.toString(),
size: [editor.size.x, editor.size.y],
config: editor.revealConfig,
frame: frame2D,
};
}
});
Loading

0 comments on commit 699302a

Please sign in to comment.