diff --git a/desktop/src/app.rs b/desktop/src/app.rs index b9db1df47c..0ec9eaa81e 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -2,6 +2,7 @@ use crate::CustomEvent; use crate::WindowSize; use crate::consts::APP_NAME; use crate::dialogs::dialog_open_graphite_file; +use crate::dialogs::dialog_save_file; use crate::dialogs::dialog_save_graphite_file; use crate::render::GraphicsState; use crate::render::WgpuContext; @@ -83,17 +84,17 @@ impl WinitApp { } for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveDocument { .. })) { - let FrontendMessage::TriggerSaveDocument { document_id, name, path, document } = message else { + let FrontendMessage::TriggerSaveDocument { document_id, name, path, content } = message else { unreachable!() }; if let Some(path) = path { - let _ = std::fs::write(&path, document); + let _ = std::fs::write(&path, content); } else { let event_loop_proxy = self.event_loop_proxy.clone(); let _ = thread::spawn(move || { let path = futures::executor::block_on(dialog_save_graphite_file(name)); if let Some(path) = path { - if let Err(e) = std::fs::write(&path, document) { + if let Err(e) = std::fs::write(&path, content) { tracing::error!("Failed to save file: {}: {}", path.display(), e); } else { let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage { @@ -107,6 +108,18 @@ impl WinitApp { } } + for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveFile { .. })) { + let FrontendMessage::TriggerSaveFile { name, content } = message else { unreachable!() }; + let _ = thread::spawn(move || { + let path = futures::executor::block_on(dialog_save_file(name)); + if let Some(path) = path { + if let Err(e) = std::fs::write(&path, content) { + tracing::error!("Failed to save file: {}: {}", path.display(), e); + } + } + }); + } + for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerVisitLink { .. })) { let _ = thread::spawn(move || { let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() }; diff --git a/desktop/src/dialogs.rs b/desktop/src/dialogs.rs index a1b2f6dbd3..a4a70f0224 100644 --- a/desktop/src/dialogs.rs +++ b/desktop/src/dialogs.rs @@ -20,3 +20,7 @@ pub(crate) async fn dialog_save_graphite_file(name: String) -> Option { .await .map(|f| f.path().to_path_buf()) } + +pub(crate) async fn dialog_save_file(name: String) -> Option { + AsyncFileDialog::new().set_title("Save File").set_file_name(name).save_file().await.map(|f| f.path().to_path_buf()) +} diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index b342396f74..c6e48d3ecb 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -68,18 +68,18 @@ pub enum FrontendMessage { document_id: DocumentId, name: String, path: Option, - document: String, + content: Vec, + }, + TriggerSaveFile { + name: String, + content: Vec, }, - TriggerDownloadImage { + TriggerExportImage { svg: String, name: String, mime: String, size: (f64, f64), }, - TriggerDownloadTextFile { - document: String, - name: String, - }, TriggerFetchAndOpenDocument { name: String, filename: String, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 9076d0354e..5bfa0af7dd 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1005,7 +1005,7 @@ impl MessageHandler> for DocumentMes document_id, name, path: self.path.clone(), - document: self.serialize_document(), + content: self.serialize_document().into_bytes(), }) } DocumentMessage::SavedDocument { path } => { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index f354020f85..06e9aab9f5 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -234,11 +234,11 @@ impl NodeGraphExecutor { }; if file_type == FileType::Svg { - responses.add(FrontendMessage::TriggerDownloadTextFile { document: svg, name }); + responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() }); } else { let mime = file_type.to_mime().to_string(); let size = (size * scale_factor).into(); - responses.add(FrontendMessage::TriggerDownloadImage { svg, name, mime, size }); + responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size }); } Ok(()) } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index ddf2fcd1d6..2d347cbfa1 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -798,10 +798,10 @@ export class TriggerSaveDocument extends JsMessage { readonly path!: string | undefined; - readonly document!: string; + readonly content!: Uint8Array; } -export class TriggerDownloadImage extends JsMessage { +export class TriggerExportImage extends JsMessage { readonly svg!: string; readonly name!: string; @@ -812,10 +812,10 @@ export class TriggerDownloadImage extends JsMessage { readonly size!: XY; } -export class TriggerDownloadTextFile extends JsMessage { - readonly document!: string; - +export class TriggerSaveFile extends JsMessage { readonly name!: string; + + readonly content!: Uint8Array; } export class TriggerSavePreferences extends JsMessage { @@ -1658,8 +1658,8 @@ export const messageMakers: Record = { SendUIMetadata, TriggerAboutGraphiteLocalizedCommitDate, TriggerSaveDocument, - TriggerDownloadImage, - TriggerDownloadTextFile, + TriggerSaveFile, + TriggerExportImage, TriggerFetchAndOpenDocument, TriggerFontLoad, TriggerImport, diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 43a5650603..4305ccc62d 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -7,8 +7,8 @@ import { type FrontendDocumentDetails, TriggerFetchAndOpenDocument, TriggerSaveDocument, - TriggerDownloadImage, - TriggerDownloadTextFile, + TriggerExportImage, + TriggerSaveFile, TriggerImport, TriggerOpenDocument, UpdateActiveDocument, @@ -18,7 +18,7 @@ import { patchWidgetLayout, UpdateSpreadsheetLayout, } from "@graphite/messages"; -import { downloadFileText, downloadFileBlob, upload } from "@graphite/utility-functions/files"; +import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files"; import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -86,13 +86,13 @@ export function createPortfolioState(editor: Editor) { editor.handle.pasteImage(data.filename, new Uint8Array(imageData.data), imageData.width, imageData.height); }); editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (triggerSaveDocument) => { - downloadFileText(triggerSaveDocument.name, triggerSaveDocument.document); + downloadFile(triggerSaveDocument.name, triggerSaveDocument.content); }); - editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => { - downloadFileText(triggerFileDownload.name, triggerFileDownload.document); + editor.subscriptions.subscribeJsMessage(TriggerSaveFile, (triggerFileDownload) => { + downloadFile(triggerFileDownload.name, triggerFileDownload.content); }); - editor.subscriptions.subscribeJsMessage(TriggerDownloadImage, async (triggerDownloadImage) => { - const { svg, name, mime, size } = triggerDownloadImage; + editor.subscriptions.subscribeJsMessage(TriggerExportImage, async (TriggerExportImage) => { + const { svg, name, mime, size } = TriggerExportImage; // Fill the canvas with white if it'll be a JPEG (which does not support transparency and defaults to black) const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined; diff --git a/frontend/src/utility-functions/files.ts b/frontend/src/utility-functions/files.ts index 610d4993d8..8fc047368c 100644 --- a/frontend/src/utility-functions/files.ts +++ b/frontend/src/utility-functions/files.ts @@ -15,10 +15,10 @@ export function downloadFileBlob(filename: string, blob: Blob) { URL.revokeObjectURL(url); } -export function downloadFileText(filename: string, text: string) { - const type = filename.endsWith(".svg") ? "image/svg+xml;charset=utf-8" : "text/plain;charset=utf-8"; +export function downloadFile(filename: string, content: Uint8Array) { + const type = filename.endsWith(".svg") ? "image/svg+xml;charset=utf-8" : "application/octet-stream"; - const blob = new Blob([text], { type }); + const blob = new Blob([content], { type }); downloadFileBlob(filename, blob); }