Skip to content

Commit 96a1b12

Browse files
Desktop: Unify save file handling and add file dialog for export (#3008)
* Prepare save file unification * Desktop add save file dialog
1 parent 5f2432c commit 96a1b12

File tree

8 files changed

+47
-30
lines changed

8 files changed

+47
-30
lines changed

desktop/src/app.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::CustomEvent;
22
use crate::WindowSize;
33
use crate::consts::APP_NAME;
44
use crate::dialogs::dialog_open_graphite_file;
5+
use crate::dialogs::dialog_save_file;
56
use crate::dialogs::dialog_save_graphite_file;
67
use crate::render::GraphicsState;
78
use crate::render::WgpuContext;
@@ -83,17 +84,17 @@ impl WinitApp {
8384
}
8485

8586
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveDocument { .. })) {
86-
let FrontendMessage::TriggerSaveDocument { document_id, name, path, document } = message else {
87+
let FrontendMessage::TriggerSaveDocument { document_id, name, path, content } = message else {
8788
unreachable!()
8889
};
8990
if let Some(path) = path {
90-
let _ = std::fs::write(&path, document);
91+
let _ = std::fs::write(&path, content);
9192
} else {
9293
let event_loop_proxy = self.event_loop_proxy.clone();
9394
let _ = thread::spawn(move || {
9495
let path = futures::executor::block_on(dialog_save_graphite_file(name));
9596
if let Some(path) = path {
96-
if let Err(e) = std::fs::write(&path, document) {
97+
if let Err(e) = std::fs::write(&path, content) {
9798
tracing::error!("Failed to save file: {}: {}", path.display(), e);
9899
} else {
99100
let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage {
@@ -107,6 +108,18 @@ impl WinitApp {
107108
}
108109
}
109110

111+
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveFile { .. })) {
112+
let FrontendMessage::TriggerSaveFile { name, content } = message else { unreachable!() };
113+
let _ = thread::spawn(move || {
114+
let path = futures::executor::block_on(dialog_save_file(name));
115+
if let Some(path) = path {
116+
if let Err(e) = std::fs::write(&path, content) {
117+
tracing::error!("Failed to save file: {}: {}", path.display(), e);
118+
}
119+
}
120+
});
121+
}
122+
110123
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerVisitLink { .. })) {
111124
let _ = thread::spawn(move || {
112125
let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() };

desktop/src/dialogs.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ pub(crate) async fn dialog_save_graphite_file(name: String) -> Option<PathBuf> {
2020
.await
2121
.map(|f| f.path().to_path_buf())
2222
}
23+
24+
pub(crate) async fn dialog_save_file(name: String) -> Option<PathBuf> {
25+
AsyncFileDialog::new().set_title("Save File").set_file_name(name).save_file().await.map(|f| f.path().to_path_buf())
26+
}

editor/src/messages/frontend/frontend_message.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,18 @@ pub enum FrontendMessage {
6868
document_id: DocumentId,
6969
name: String,
7070
path: Option<PathBuf>,
71-
document: String,
71+
content: Vec<u8>,
72+
},
73+
TriggerSaveFile {
74+
name: String,
75+
content: Vec<u8>,
7276
},
73-
TriggerDownloadImage {
77+
TriggerExportImage {
7478
svg: String,
7579
name: String,
7680
mime: String,
7781
size: (f64, f64),
7882
},
79-
TriggerDownloadTextFile {
80-
document: String,
81-
name: String,
82-
},
8383
TriggerFetchAndOpenDocument {
8484
name: String,
8585
filename: String,

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
10051005
document_id,
10061006
name,
10071007
path: self.path.clone(),
1008-
document: self.serialize_document(),
1008+
content: self.serialize_document().into_bytes(),
10091009
})
10101010
}
10111011
DocumentMessage::SavedDocument { path } => {

editor/src/node_graph_executor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,11 @@ impl NodeGraphExecutor {
234234
};
235235

236236
if file_type == FileType::Svg {
237-
responses.add(FrontendMessage::TriggerDownloadTextFile { document: svg, name });
237+
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
238238
} else {
239239
let mime = file_type.to_mime().to_string();
240240
let size = (size * scale_factor).into();
241-
responses.add(FrontendMessage::TriggerDownloadImage { svg, name, mime, size });
241+
responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
242242
}
243243
Ok(())
244244
}

frontend/src/messages.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -798,10 +798,10 @@ export class TriggerSaveDocument extends JsMessage {
798798

799799
readonly path!: string | undefined;
800800

801-
readonly document!: string;
801+
readonly content!: Uint8Array;
802802
}
803803

804-
export class TriggerDownloadImage extends JsMessage {
804+
export class TriggerExportImage extends JsMessage {
805805
readonly svg!: string;
806806

807807
readonly name!: string;
@@ -812,10 +812,10 @@ export class TriggerDownloadImage extends JsMessage {
812812
readonly size!: XY;
813813
}
814814

815-
export class TriggerDownloadTextFile extends JsMessage {
816-
readonly document!: string;
817-
815+
export class TriggerSaveFile extends JsMessage {
818816
readonly name!: string;
817+
818+
readonly content!: Uint8Array;
819819
}
820820

821821
export class TriggerSavePreferences extends JsMessage {
@@ -1658,8 +1658,8 @@ export const messageMakers: Record<string, MessageMaker> = {
16581658
SendUIMetadata,
16591659
TriggerAboutGraphiteLocalizedCommitDate,
16601660
TriggerSaveDocument,
1661-
TriggerDownloadImage,
1662-
TriggerDownloadTextFile,
1661+
TriggerSaveFile,
1662+
TriggerExportImage,
16631663
TriggerFetchAndOpenDocument,
16641664
TriggerFontLoad,
16651665
TriggerImport,

frontend/src/state-providers/portfolio.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
type FrontendDocumentDetails,
88
TriggerFetchAndOpenDocument,
99
TriggerSaveDocument,
10-
TriggerDownloadImage,
11-
TriggerDownloadTextFile,
10+
TriggerExportImage,
11+
TriggerSaveFile,
1212
TriggerImport,
1313
TriggerOpenDocument,
1414
UpdateActiveDocument,
@@ -18,7 +18,7 @@ import {
1818
patchWidgetLayout,
1919
UpdateSpreadsheetLayout,
2020
} from "@graphite/messages";
21-
import { downloadFileText, downloadFileBlob, upload } from "@graphite/utility-functions/files";
21+
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
2222
import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization";
2323

2424
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@@ -86,13 +86,13 @@ export function createPortfolioState(editor: Editor) {
8686
editor.handle.pasteImage(data.filename, new Uint8Array(imageData.data), imageData.width, imageData.height);
8787
});
8888
editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (triggerSaveDocument) => {
89-
downloadFileText(triggerSaveDocument.name, triggerSaveDocument.document);
89+
downloadFile(triggerSaveDocument.name, triggerSaveDocument.content);
9090
});
91-
editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => {
92-
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
91+
editor.subscriptions.subscribeJsMessage(TriggerSaveFile, (triggerFileDownload) => {
92+
downloadFile(triggerFileDownload.name, triggerFileDownload.content);
9393
});
94-
editor.subscriptions.subscribeJsMessage(TriggerDownloadImage, async (triggerDownloadImage) => {
95-
const { svg, name, mime, size } = triggerDownloadImage;
94+
editor.subscriptions.subscribeJsMessage(TriggerExportImage, async (TriggerExportImage) => {
95+
const { svg, name, mime, size } = TriggerExportImage;
9696

9797
// Fill the canvas with white if it'll be a JPEG (which does not support transparency and defaults to black)
9898
const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined;

frontend/src/utility-functions/files.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export function downloadFileBlob(filename: string, blob: Blob) {
1515
URL.revokeObjectURL(url);
1616
}
1717

18-
export function downloadFileText(filename: string, text: string) {
19-
const type = filename.endsWith(".svg") ? "image/svg+xml;charset=utf-8" : "text/plain;charset=utf-8";
18+
export function downloadFile(filename: string, content: Uint8Array) {
19+
const type = filename.endsWith(".svg") ? "image/svg+xml;charset=utf-8" : "application/octet-stream";
2020

21-
const blob = new Blob([text], { type });
21+
const blob = new Blob([content], { type });
2222
downloadFileBlob(filename, blob);
2323
}
2424

0 commit comments

Comments
 (0)