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 18:07:53 +02:00 committed by GitHub
parent 5f2432cacf
commit 96a1b12a05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 47 additions and 30 deletions

View File

@ -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!() };

View File

@ -20,3 +20,7 @@ pub(crate) async fn dialog_save_graphite_file(name: String) -> Option<PathBuf> {
.await
.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())
}

View File

@ -68,18 +68,18 @@ pub enum FrontendMessage {
document_id: DocumentId,
name: String,
path: Option<PathBuf>,
document: String,
content: Vec<u8>,
},
TriggerDownloadImage {
TriggerSaveFile {
name: String,
content: Vec<u8>,
},
TriggerExportImage {
svg: String,
name: String,
mime: String,
size: (f64, f64),
},
TriggerDownloadTextFile {
document: String,
name: String,
},
TriggerFetchAndOpenDocument {
name: String,
filename: String,

View File

@ -1005,7 +1005,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
document_id,
name,
path: self.path.clone(),
document: self.serialize_document(),
content: self.serialize_document().into_bytes(),
})
}
DocumentMessage::SavedDocument { path } => {

View File

@ -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(())
}

View File

@ -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<string, MessageMaker> = {
SendUIMetadata,
TriggerAboutGraphiteLocalizedCommitDate,
TriggerSaveDocument,
TriggerDownloadImage,
TriggerDownloadTextFile,
TriggerSaveFile,
TriggerExportImage,
TriggerFetchAndOpenDocument,
TriggerFontLoad,
TriggerImport,

View File

@ -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;

View File

@ -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);
}