mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-06 03:28:13 +08:00
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:
+16
-3
@@ -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!() };
|
||||||
|
|||||||
@@ -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 } => {
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user