From 748078a861afc3396a6d67abc1ab2be69983827e Mon Sep 17 00:00:00 2001 From: iskaktoltay Date: Thu, 25 Jul 2024 13:05:37 +0500 Subject: [PATCH] made exporting media files in a separate media folder --- frontend/apps/desktop/src/main.ts | 27 +----- frontend/apps/desktop/src/root.tsx | 8 +- .../apps/desktop/src/save-markdown-file.tsx | 92 +++++++++++++++++++ frontend/packages/app/app-context.tsx | 12 ++- .../app/components/export-doc-button.tsx | 7 +- .../packages/app/utils/blocks-to-markdown.ts | 57 +++++++++--- 6 files changed, 159 insertions(+), 44 deletions(-) create mode 100644 frontend/apps/desktop/src/save-markdown-file.tsx diff --git a/frontend/apps/desktop/src/main.ts b/frontend/apps/desktop/src/main.ts index 2a9539a5a..1e183141e 100644 --- a/frontend/apps/desktop/src/main.ts +++ b/frontend/apps/desktop/src/main.ts @@ -30,6 +30,7 @@ import {appStore} from './app-store' import autoUpdate from './auto-update' import {startMainDaemon} from './daemon' import {saveCidAsFile} from './save-cid-as-file' +import {saveMarkdownFile} from './save-markdown-file' const OS_REGISTER_SCHEME = 'hm' @@ -129,32 +130,8 @@ ipcMain.handle('dark-mode:system', () => { }) ipcMain.on('save-file', saveCidAsFile) -ipcMain.on('export-document', async (_event, args) => { - const {title, markdown} = args - const camelTitle = title - .split(' ') - .map( - (word: string) => - word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), - ) - .join('') - const {filePath} = await dialog.showSaveDialog({ - title: 'Save Markdown', - defaultPath: path.join(__dirname, camelTitle + '.md'), - buttonLabel: 'Save', - filters: [{name: 'Markdown Files', extensions: ['md']}], - }) - if (filePath) { - fs.writeFile(filePath, markdown, (err) => { - if (err) { - console.error('Error saving file:', err) - return - } - console.log('File successfully saved:', filePath) - }) - } -}) +ipcMain.on('export-document', saveMarkdownFile) ipcMain.on( 'export-multiple-documents', diff --git a/frontend/apps/desktop/src/root.tsx b/frontend/apps/desktop/src/root.tsx index ade92265c..d4a838e90 100644 --- a/frontend/apps/desktop/src/root.tsx +++ b/frontend/apps/desktop/src/root.tsx @@ -237,8 +237,12 @@ function MainApp({ saveCidAsFile={async (cid: string, name: string) => { ipc.send?.('save-file', {cid, name}) }} - exportDocument={async (title: string, markdown: string) => { - ipc.send?.('export-document', {title, markdown}) + exportDocument={async ( + title: string, + markdownContent: string, + mediaFiles: {url: string; filename: string}[], + ) => { + ipc.send?.('export-document', {title, markdownContent, mediaFiles}) }} exportDocuments={async ( documents: { diff --git a/frontend/apps/desktop/src/save-markdown-file.tsx b/frontend/apps/desktop/src/save-markdown-file.tsx new file mode 100644 index 000000000..45432efdf --- /dev/null +++ b/frontend/apps/desktop/src/save-markdown-file.tsx @@ -0,0 +1,92 @@ +import {API_HTTP_URL} from '@mintter/shared' +import {app, dialog, net} from 'electron' +import fs from 'fs' +import path from 'node:path' + +const {debug, error} = console + +export async function saveMarkdownFile(event, args) { + const {title, markdownContent, mediaFiles} = args + const formattedTitle = title + .split(' ') + .map( + (word: string) => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ) + .join('') + + const {filePath} = await dialog.showSaveDialog({ + title: 'Save Markdown and Media', + defaultPath: path.join(app.getPath('documents'), formattedTitle), + buttonLabel: 'Save', + filters: [{name: 'Markdown Files', extensions: ['md']}], + }) + + if (filePath) { + const dir = path.dirname(filePath) + const documentDir = path.join(dir, formattedTitle) + + if (!fs.existsSync(documentDir)) { + fs.mkdirSync(documentDir) + } + + const mediaDir = path.join(documentDir, 'media') + if (!fs.existsSync(mediaDir)) { + fs.mkdirSync(mediaDir) + } + + // Save Markdown file + const markdownFilePath = path.join(documentDir, `${formattedTitle}.md`) + fs.writeFile(markdownFilePath, markdownContent, (err) => { + if (err) { + error('Error saving file:', err) + return + } + debug('Markdown file successfully saved:', markdownFilePath) + }) + + // Save Media files using CID + for (const {url, filename} of mediaFiles) { + const regex = /ipfs:\/\/(.+)/ + const match = url.match(regex) + const cid = match ? match[1] : null + const request = net.request(`${API_HTTP_URL}/ipfs/${cid}`) + + request.on('response', (response) => { + debug(response) + if (response.statusCode === 200) { + const chunks: Buffer[] = [] + + response.on('data', (chunk) => { + chunks.push(chunk) + }) + + response.on('end', () => { + const data = Buffer.concat(chunks) + debug('~~~~~~~~~~~~~HEREEEEEEEEEEEEEEEEE', chunks[0], data.toJSON()) + if (!data || data.length === 0) { + error(`Error: No data received for ${filename}`) + return + } + + const mediaFilePath = path.join(mediaDir, filename) + try { + fs.writeFileSync(mediaFilePath, data) + debug(`Media file successfully saved: ${mediaFilePath}`) + } catch (e) { + error(`Failed to save media file ${filename}`, e) + } + }) + } else { + error(`Error: Invalid status code ${response.statusCode}`) + } + }) + + request.on('error', (err) => { + error('Error:', err.message) + }) + + request.end() + } + } +} diff --git a/frontend/packages/app/app-context.tsx b/frontend/packages/app/app-context.tsx index 93c09c8b6..7cb0beaac 100644 --- a/frontend/packages/app/app-context.tsx +++ b/frontend/packages/app/app-context.tsx @@ -19,7 +19,11 @@ export type AppContext = { externalOpen: (url: string) => Promise windowUtils: WindowUtils saveCidAsFile: (cid: string, name: string) => Promise - exportDocument: (title: string, markdown: string) => Promise + exportDocument: ( + title: string, + markdownContent: string, + mediaFiles: {url: string; filename: string}[], + ) => Promise exportDocuments: ( documents: { title: string @@ -51,7 +55,11 @@ export function AppContextProvider({ externalOpen: (url: string) => Promise windowUtils: WindowUtils saveCidAsFile: (cid: string, name: string) => Promise - exportDocument: (title: string, markdown: string) => Promise + exportDocument: ( + title: string, + markdownContent: string, + mediaFiles: {url: string; filename: string}[], + ) => Promise exportDocuments: ( documents: { title: string diff --git a/frontend/packages/app/components/export-doc-button.tsx b/frontend/packages/app/components/export-doc-button.tsx index b864734a3..52e1cd5ed 100644 --- a/frontend/packages/app/components/export-doc-button.tsx +++ b/frontend/packages/app/components/export-doc-button.tsx @@ -25,7 +25,12 @@ export const ExportDocButton = ({ const blocks: HMBlockNode[] | undefined = pub.data?.document?.children const editorBlocks = toHMBlock(blocks) - exportDocument(title, await convertBlocksToMarkdown(editorBlocks)) + + const markdownWithFiles = + await convertBlocksToMarkdown(editorBlocks) + + const {markdownContent, mediaFiles} = markdownWithFiles + exportDocument(title, markdownContent, mediaFiles) }} icon={Download} > diff --git a/frontend/packages/app/utils/blocks-to-markdown.ts b/frontend/packages/app/utils/blocks-to-markdown.ts index 0d899464f..10373691f 100644 --- a/frontend/packages/app/utils/blocks-to-markdown.ts +++ b/frontend/packages/app/utils/blocks-to-markdown.ts @@ -31,19 +31,9 @@ function convertContentItemToHtml(contentItem) { } function convertBlockToHtml(block) { - let childrenHtml = '' - if (block.children) { - const childrenContent = block.children.map(convertBlockToHtml).join('\n') - if (block.props.childrenType === 'ul') { - childrenHtml = `
    ${childrenContent}
` - } else if (block.props.childrenType === 'ol') { - childrenHtml = `
    ${childrenContent}
` - } else { - childrenHtml = childrenContent - } - } + const childrenHtml = block.children + ? block.children.map(convertBlockToHtml).join('\n') + : '' switch (block.type) { case 'heading': @@ -68,6 +58,17 @@ function convertBlockToHtml(block) { return `

![${block.props.name}](${block.props.url} "width=${block.props.width}")

\n${childrenHtml}` case 'file': return `

[${block.props.name}](${block.props.url} "size=${block.props.size}")

\n${childrenHtml}` + case 'list': + if (block.props.childrenType === 'ul') { + return `
    ${block.children + .map((child) => `
  • ${convertBlockToHtml(child)}
  • `) + .join('\n')}
\n${childrenHtml}` + } else if (block.props.childrenType === 'ol') { + return `
    ${block.children + .map((child) => `
  1. ${convertBlockToHtml(child)}
  2. `) + .join('\n')}
\n${childrenHtml}` + } + return '' default: return block.content ? block.content.map(convertContentItemToHtml).join('') + @@ -80,15 +81,43 @@ function convertBlocksToHtml(blocks) { const htmlContent: string = blocks .map((block) => convertBlockToHtml(block)) .join('\n\n') + console.log(htmlContent) return htmlContent } +async function extractMediaFiles(blocks) { + const mediaFiles: {url: string; filename: string}[] = [] + const extractMedia = async (block) => { + if ( + block.type === 'image' || + block.type === 'video' || + block.type === 'file' + ) { + const url = block.props.url + const filename = url.split('/').pop() + mediaFiles.push({url, filename}) + block.props = {...block.props, url: `media/${filename}`} // Update the URL to point to the local media folder + } + if (block.children) { + for (const child of block.children) { + await extractMedia(child) + } + } + } + for (const block of blocks) { + await extractMedia(block) + } + return mediaFiles +} + export async function convertBlocksToMarkdown(blocks: HMBlock[]) { + const mediaFiles = await extractMediaFiles(blocks) // Extract media files and update URLs first const markdownFile = await unified() .use(rehypeParse, {fragment: true}) .use(rehypeRemark) .use(remarkGfm) .use(remarkStringify) .process(convertBlocksToHtml(blocks)) - return markdownFile.value as string + const markdownContent = markdownFile.value as string + return {markdownContent, mediaFiles} }