Desktop: Unify save file handling and add file dialog for export (#3008)

* Prepare save file unification

* Desktop add save file dialog
This commit is contained in:
Timon
2025-08-06 16:07:53 +00:00
committed by GitHub
parent 5f2432cacf
commit 96a1b12a05
8 changed files with 47 additions and 30 deletions
+16 -3
View File
@@ -2,6 +2,7 @@ use crate::CustomEvent;
use crate::WindowSize; use crate::WindowSize;
use crate::consts::APP_NAME; use crate::consts::APP_NAME;
use crate::dialogs::dialog_open_graphite_file; use crate::dialogs::dialog_open_graphite_file;
use crate::dialogs::dialog_save_file;
use crate::dialogs::dialog_save_graphite_file; use crate::dialogs::dialog_save_graphite_file;
use crate::render::GraphicsState; use crate::render::GraphicsState;
use crate::render::WgpuContext; use crate::render::WgpuContext;
@@ -83,17 +84,17 @@ impl WinitApp {
} }
for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerSaveDocument { .. })) { 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!() unreachable!()
}; };
if let Some(path) = path { if let Some(path) = path {
let _ = std::fs::write(&path, document); let _ = std::fs::write(&path, content);
} else { } else {
let event_loop_proxy = self.event_loop_proxy.clone(); let event_loop_proxy = self.event_loop_proxy.clone();
let _ = thread::spawn(move || { let _ = thread::spawn(move || {
let path = futures::executor::block_on(dialog_save_graphite_file(name)); let path = futures::executor::block_on(dialog_save_graphite_file(name));
if let Some(path) = path { 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); tracing::error!("Failed to save file: {}: {}", path.display(), e);
} else { } else {
let message = Message::Portfolio(PortfolioMessage::DocumentPassMessage { 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 { .. })) { for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::TriggerVisitLink { .. })) {
let _ = thread::spawn(move || { let _ = thread::spawn(move || {
let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() }; let FrontendMessage::TriggerVisitLink { url } = message else { unreachable!() };
+4
View File
@@ -20,3 +20,7 @@ pub(crate) async fn dialog_save_graphite_file(name: String) -> Option<PathBuf> {
.await .await
.map(|f| f.path().to_path_buf()) .map(|f| f.path().to_path_buf())
} }
pub(crate) async fn dialog_save_file(name: String) -> Option<PathBuf> {
AsyncFileDialog::new().set_title("Save File").set_file_name(name).save_file().await.map(|f| f.path().to_path_buf())
}
@@ -68,18 +68,18 @@ pub enum FrontendMessage {
document_id: DocumentId, document_id: DocumentId,
name: String, name: String,
path: Option<PathBuf>, path: Option<PathBuf>,
document: String, content: Vec<u8>,
}, },
TriggerDownloadImage { TriggerSaveFile {
name: String,
content: Vec<u8>,
},
TriggerExportImage {
svg: String, svg: String,
name: String, name: String,
mime: String, mime: String,
size: (f64, f64), size: (f64, f64),
}, },
TriggerDownloadTextFile {
document: String,
name: String,
},
TriggerFetchAndOpenDocument { TriggerFetchAndOpenDocument {
name: String, name: String,
filename: String, filename: String,
@@ -1005,7 +1005,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
document_id, document_id,
name, name,
path: self.path.clone(), path: self.path.clone(),
document: self.serialize_document(), content: self.serialize_document().into_bytes(),
}) })
} }
DocumentMessage::SavedDocument { path } => { DocumentMessage::SavedDocument { path } => {
+2 -2
View File
@@ -234,11 +234,11 @@ impl NodeGraphExecutor {
}; };
if file_type == FileType::Svg { if file_type == FileType::Svg {
responses.add(FrontendMessage::TriggerDownloadTextFile { document: svg, name }); responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
} else { } else {
let mime = file_type.to_mime().to_string(); let mime = file_type.to_mime().to_string();
let size = (size * scale_factor).into(); let size = (size * scale_factor).into();
responses.add(FrontendMessage::TriggerDownloadImage { svg, name, mime, size }); responses.add(FrontendMessage::TriggerExportImage { svg, name, mime, size });
} }
Ok(()) Ok(())
} }
+7 -7
View File
@@ -798,10 +798,10 @@ export class TriggerSaveDocument extends JsMessage {
readonly path!: string | undefined; readonly path!: string | undefined;
readonly document!: string; readonly content!: Uint8Array;
} }
export class TriggerDownloadImage extends JsMessage { export class TriggerExportImage extends JsMessage {
readonly svg!: string; readonly svg!: string;
readonly name!: string; readonly name!: string;
@@ -812,10 +812,10 @@ export class TriggerDownloadImage extends JsMessage {
readonly size!: XY; readonly size!: XY;
} }
export class TriggerDownloadTextFile extends JsMessage { export class TriggerSaveFile extends JsMessage {
readonly document!: string;
readonly name!: string; readonly name!: string;
readonly content!: Uint8Array;
} }
export class TriggerSavePreferences extends JsMessage { export class TriggerSavePreferences extends JsMessage {
@@ -1658,8 +1658,8 @@ export const messageMakers: Record<string, MessageMaker> = {
SendUIMetadata, SendUIMetadata,
TriggerAboutGraphiteLocalizedCommitDate, TriggerAboutGraphiteLocalizedCommitDate,
TriggerSaveDocument, TriggerSaveDocument,
TriggerDownloadImage, TriggerSaveFile,
TriggerDownloadTextFile, TriggerExportImage,
TriggerFetchAndOpenDocument, TriggerFetchAndOpenDocument,
TriggerFontLoad, TriggerFontLoad,
TriggerImport, TriggerImport,
+8 -8
View File
@@ -7,8 +7,8 @@ import {
type FrontendDocumentDetails, type FrontendDocumentDetails,
TriggerFetchAndOpenDocument, TriggerFetchAndOpenDocument,
TriggerSaveDocument, TriggerSaveDocument,
TriggerDownloadImage, TriggerExportImage,
TriggerDownloadTextFile, TriggerSaveFile,
TriggerImport, TriggerImport,
TriggerOpenDocument, TriggerOpenDocument,
UpdateActiveDocument, UpdateActiveDocument,
@@ -18,7 +18,7 @@ import {
patchWidgetLayout, patchWidgetLayout,
UpdateSpreadsheetLayout, UpdateSpreadsheetLayout,
} from "@graphite/messages"; } 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"; import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // 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.handle.pasteImage(data.filename, new Uint8Array(imageData.data), imageData.width, imageData.height);
}); });
editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (triggerSaveDocument) => { editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (triggerSaveDocument) => {
downloadFileText(triggerSaveDocument.name, triggerSaveDocument.document); downloadFile(triggerSaveDocument.name, triggerSaveDocument.content);
}); });
editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => { editor.subscriptions.subscribeJsMessage(TriggerSaveFile, (triggerFileDownload) => {
downloadFileText(triggerFileDownload.name, triggerFileDownload.document); downloadFile(triggerFileDownload.name, triggerFileDownload.content);
}); });
editor.subscriptions.subscribeJsMessage(TriggerDownloadImage, async (triggerDownloadImage) => { editor.subscriptions.subscribeJsMessage(TriggerExportImage, async (TriggerExportImage) => {
const { svg, name, mime, size } = triggerDownloadImage; 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) // 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; const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined;
+3 -3
View File
@@ -15,10 +15,10 @@ export function downloadFileBlob(filename: string, blob: Blob) {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
export function downloadFileText(filename: string, text: string) { export function downloadFile(filename: string, content: Uint8Array) {
const type = filename.endsWith(".svg") ? "image/svg+xml;charset=utf-8" : "text/plain;charset=utf-8"; 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); downloadFileBlob(filename, blob);
} }