Desktop: Add File > Save As… (#3034)
* Make file name and document name identical * Add save as action * Fix test errors * Add missing save as action * Desktop fix drop file open document file message * Address review comments * Replace file save suffix with file extension * Add comment specifying that the upload function takes a html input accept string * Fix remove file extension in web * Use let * Don't show save as menu entry in web * Don't add SaveDocumentAs in web * Remove file extension on all open document file calls --------- Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
parent
7c30f6168b
commit
e70862b399
|
|
@ -10,7 +10,6 @@ use graph_craft::wasm_application_io::WasmApplicationIo;
|
|||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphite_editor::application::Editor;
|
||||
use graphite_editor::consts::DEFAULT_DOCUMENT_NAME;
|
||||
use graphite_editor::messages::prelude::*;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -79,7 +78,8 @@ impl WinitApp {
|
|||
String::new()
|
||||
});
|
||||
let message = PortfolioMessage::OpenDocumentFile {
|
||||
document_name: path.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown").to_string(),
|
||||
document_name: None,
|
||||
document_path: Some(path),
|
||||
document_serialized_content: content,
|
||||
};
|
||||
let _ = event_loop_proxy.send_event(CustomEvent::DispatchMessage(message.into()));
|
||||
|
|
@ -294,7 +294,8 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
|||
let Some(content) = load_string(&path) else { return };
|
||||
|
||||
let message = PortfolioMessage::OpenDocumentFile {
|
||||
document_name: name.unwrap_or(DEFAULT_DOCUMENT_NAME.to_string()),
|
||||
document_name: None,
|
||||
document_path: Some(path),
|
||||
document_serialized_content: content,
|
||||
};
|
||||
self.dispatch_message(message.into());
|
||||
|
|
|
|||
|
|
@ -150,8 +150,8 @@ pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
|||
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
|
||||
|
||||
// DOCUMENT
|
||||
pub const FILE_EXTENSION: &str = "graphite";
|
||||
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
||||
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
||||
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -497,7 +497,8 @@ mod test {
|
|||
);
|
||||
|
||||
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
document_name: document_name.into(),
|
||||
document_name: Some(document_name.to_string()),
|
||||
document_path: None,
|
||||
document_serialized_content,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
|
|||
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
||||
|
||||
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
||||
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
file_type: self.file_type,
|
||||
scale_factor: self.scale_factor,
|
||||
bounds: self.bounds,
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
|
||||
entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs),
|
||||
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ pub enum DocumentMessage {
|
|||
RenderRulers,
|
||||
RenderScrollbars,
|
||||
SaveDocument,
|
||||
SaveDocumentAs,
|
||||
SavedDocument {
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO
|
|||
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
|
||||
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
|
||||
use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
||||
|
|
@ -85,8 +85,6 @@ pub struct DocumentMessageHandler {
|
|||
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
|
||||
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
|
||||
pub collapsed: CollapsedLayers,
|
||||
/// The name of the document, which is displayed in the tab and title bar of the editor.
|
||||
pub name: String,
|
||||
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
||||
/// We save this to provide a hint about which version of the editor was used to create the document.
|
||||
pub commit_hash: String,
|
||||
|
|
@ -113,6 +111,12 @@ pub struct DocumentMessageHandler {
|
|||
// Fields omitted from the saved document format
|
||||
// =============================================
|
||||
//
|
||||
/// The name of the document, which is displayed in the tab and title bar of the editor.
|
||||
#[serde(skip)]
|
||||
pub name: String,
|
||||
/// The path of the to the document file.
|
||||
#[serde(skip)]
|
||||
pub(crate) path: Option<PathBuf>,
|
||||
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
|
||||
#[serde(skip)]
|
||||
breadcrumb_network_path: Vec<NodeId>,
|
||||
|
|
@ -125,9 +129,6 @@ pub struct DocumentMessageHandler {
|
|||
/// Stack of document network snapshots for future history states.
|
||||
#[serde(skip)]
|
||||
document_redo_history: VecDeque<NodeNetworkInterface>,
|
||||
/// The path of the to the document file.
|
||||
#[serde(skip)]
|
||||
path: Option<PathBuf>,
|
||||
/// Hash of the document snapshot that was most recently saved to disk by the user.
|
||||
#[serde(skip)]
|
||||
saved_hash: Option<u64>,
|
||||
|
|
@ -159,7 +160,6 @@ impl Default for DocumentMessageHandler {
|
|||
// ============================================
|
||||
network_interface: default_document_network_interface(),
|
||||
collapsed: CollapsedLayers::default(),
|
||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
|
||||
document_ptz: PTZ::default(),
|
||||
document_mode: DocumentMode::DesignMode,
|
||||
|
|
@ -172,11 +172,12 @@ impl Default for DocumentMessageHandler {
|
|||
// =============================================
|
||||
// Fields omitted from the saved document format
|
||||
// =============================================
|
||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||
path: None,
|
||||
breadcrumb_network_path: Vec::new(),
|
||||
selection_network_path: Vec::new(),
|
||||
document_undo_history: VecDeque::new(),
|
||||
document_redo_history: VecDeque::new(),
|
||||
path: None,
|
||||
saved_hash: None,
|
||||
auto_saved_hash: None,
|
||||
layer_range_selection_reference: None,
|
||||
|
|
@ -947,7 +948,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
DocumentMessage::RenameDocument { new_name } => {
|
||||
self.name = new_name;
|
||||
self.name = new_name.clone();
|
||||
|
||||
self.path = None;
|
||||
self.set_save_state(false);
|
||||
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
|
|
@ -1020,25 +1025,40 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
multiplier: scrollbar_multiplier.into(),
|
||||
});
|
||||
}
|
||||
DocumentMessage::SaveDocument => {
|
||||
DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => {
|
||||
if let DocumentMessage::SaveDocumentAs = message {
|
||||
self.path = None;
|
||||
}
|
||||
|
||||
self.set_save_state(true);
|
||||
responses.add(PortfolioMessage::AutoSaveActiveDocument);
|
||||
// Update the save status of the just saved document
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
|
||||
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
||||
true => self.name.clone(),
|
||||
false => self.name.clone() + FILE_SAVE_SUFFIX,
|
||||
};
|
||||
responses.add(FrontendMessage::TriggerSaveDocument {
|
||||
document_id,
|
||||
name,
|
||||
name: format!("{}.{}", self.name.clone(), FILE_EXTENSION),
|
||||
path: self.path.clone(),
|
||||
content: self.serialize_document().into_bytes(),
|
||||
})
|
||||
}
|
||||
DocumentMessage::SavedDocument { path } => {
|
||||
self.path = path;
|
||||
|
||||
// Update the name to match the file stem
|
||||
let document_name_from_path = self.path.as_ref().and_then(|path| {
|
||||
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
|
||||
path.file_stem().map(|n| n.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(name) = document_name_from_path {
|
||||
self.name = name;
|
||||
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
}
|
||||
DocumentMessage::SelectParentLayer => {
|
||||
let selected_nodes = self.network_interface.selected_nodes();
|
||||
|
|
@ -1571,6 +1591,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||
ZoomCanvasToFitAll,
|
||||
);
|
||||
|
||||
// Additional actions available on desktop
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
common.extend(actions!(DocumentMessageDiscriminant::SaveDocumentAs));
|
||||
|
||||
// Additional actions if there are any selected layers
|
||||
if self.network_interface.selected_nodes().selected_layers(self.metadata()).next().is_some() {
|
||||
let mut select = actions!(DocumentMessageDiscriminant;
|
||||
|
|
|
|||
|
|
@ -101,14 +101,25 @@ impl LayoutHolder for MenuBarMessageHandler {
|
|||
..MenuBarEntry::default()
|
||||
},
|
||||
],
|
||||
vec![MenuBarEntry {
|
||||
vec![
|
||||
MenuBarEntry {
|
||||
label: "Save".into(),
|
||||
icon: Some("Save".into()),
|
||||
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
|
||||
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
|
||||
disabled: no_active_document,
|
||||
..MenuBarEntry::default()
|
||||
}],
|
||||
},
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
MenuBarEntry {
|
||||
label: "Save As…".into(),
|
||||
icon: Some("Save".into()),
|
||||
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs),
|
||||
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()),
|
||||
disabled: no_active_document,
|
||||
..MenuBarEntry::default()
|
||||
},
|
||||
],
|
||||
vec![
|
||||
MenuBarEntry {
|
||||
label: "Import…".into(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::messages::prelude::*;
|
|||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphene_std::text::Font;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[impl_message(Message, Portfolio)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
|
|
@ -66,18 +67,20 @@ pub enum PortfolioMessage {
|
|||
NextDocument,
|
||||
OpenDocument,
|
||||
OpenDocumentFile {
|
||||
document_name: String,
|
||||
document_name: Option<String>,
|
||||
document_path: Option<PathBuf>,
|
||||
document_serialized_content: String,
|
||||
},
|
||||
ToggleResetNodesToDefinitionsOnOpen,
|
||||
OpenDocumentFileWithId {
|
||||
document_id: DocumentId,
|
||||
document_name: String,
|
||||
document_name: Option<String>,
|
||||
document_path: Option<PathBuf>,
|
||||
document_is_auto_saved: bool,
|
||||
document_is_saved: bool,
|
||||
document_serialized_content: String,
|
||||
to_front: bool,
|
||||
},
|
||||
ToggleResetNodesToDefinitionsOnOpen,
|
||||
PasteIntoFolder {
|
||||
clipboard: Clipboard,
|
||||
parent: LayerNodeIdentifier,
|
||||
|
|
@ -115,7 +118,7 @@ pub enum PortfolioMessage {
|
|||
document_id: DocumentId,
|
||||
},
|
||||
SubmitDocumentExport {
|
||||
file_name: String,
|
||||
name: String,
|
||||
file_type: FileType,
|
||||
scale_factor: f64,
|
||||
bounds: ExportBounds,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
|||
use super::document::utility_types::network_interface;
|
||||
use super::utility_types::{PanelType, PersistentData};
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH};
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
||||
use crate::messages::animation::TimingInformation;
|
||||
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
|
||||
use crate::messages::dialog::simple_dialogs;
|
||||
|
|
@ -419,12 +419,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
PortfolioMessage::OpenDocumentFile {
|
||||
document_name,
|
||||
document_path,
|
||||
document_serialized_content,
|
||||
} => {
|
||||
let document_id = DocumentId(generate_uuid());
|
||||
responses.add(PortfolioMessage::OpenDocumentFileWithId {
|
||||
document_id,
|
||||
document_name,
|
||||
document_path,
|
||||
document_is_auto_saved: false,
|
||||
document_is_saved: true,
|
||||
document_serialized_content,
|
||||
|
|
@ -439,6 +441,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
PortfolioMessage::OpenDocumentFileWithId {
|
||||
document_id,
|
||||
document_name,
|
||||
document_path,
|
||||
document_is_auto_saved,
|
||||
document_is_saved,
|
||||
document_serialized_content,
|
||||
|
|
@ -450,10 +453,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);
|
||||
|
||||
// Deserialize the document
|
||||
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
|
||||
document.name.clone_from(&document_name);
|
||||
document
|
||||
});
|
||||
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content);
|
||||
|
||||
// Display an error to the user if the document could not be opened
|
||||
let mut document = match document {
|
||||
|
|
@ -514,6 +514,30 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
document.set_auto_save_state(document_is_auto_saved);
|
||||
document.set_save_state(document_is_saved);
|
||||
|
||||
let document_name_from_path = document_path.as_ref().and_then(|path| {
|
||||
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
|
||||
path.file_stem().map(|n| n.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
match (document_name, document_path, document_name_from_path) {
|
||||
(Some(name), _, None) => {
|
||||
document.name = name;
|
||||
}
|
||||
(_, Some(path), Some(name)) => {
|
||||
document.name = name;
|
||||
document.path = Some(path);
|
||||
}
|
||||
(_, _, Some(name)) => {
|
||||
document.name = name;
|
||||
}
|
||||
_ => {
|
||||
document.name = DEFAULT_DOCUMENT_NAME.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Load the document into the portfolio so it opens in the editor
|
||||
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
|
||||
}
|
||||
|
|
@ -899,7 +923,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
}
|
||||
PortfolioMessage::SubmitDocumentExport {
|
||||
file_name,
|
||||
name,
|
||||
file_type,
|
||||
scale_factor,
|
||||
bounds,
|
||||
|
|
@ -907,7 +931,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
} => {
|
||||
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document");
|
||||
let export_config = ExportConfig {
|
||||
file_name,
|
||||
name,
|
||||
file_type,
|
||||
scale_factor,
|
||||
bounds,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::consts::FILE_SAVE_SUFFIX;
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2, UVec2};
|
||||
|
|
@ -229,18 +228,11 @@ impl NodeGraphExecutor {
|
|||
};
|
||||
|
||||
let ExportConfig {
|
||||
file_type,
|
||||
file_name,
|
||||
size,
|
||||
scale_factor,
|
||||
..
|
||||
file_type, name, size, scale_factor, ..
|
||||
} = export_config;
|
||||
|
||||
let file_suffix = &format!(".{file_type:?}").to_lowercase();
|
||||
let name = match file_name.ends_with(FILE_SAVE_SUFFIX) {
|
||||
true => file_name.replace(FILE_SAVE_SUFFIX, file_suffix),
|
||||
false => file_name + file_suffix,
|
||||
};
|
||||
let name = name + file_suffix;
|
||||
|
||||
if file_type == FileType::Svg {
|
||||
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ pub struct GraphUpdate {
|
|||
|
||||
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ExportConfig {
|
||||
pub file_name: String,
|
||||
pub name: String,
|
||||
pub file_type: FileType,
|
||||
pub scale_factor: f64,
|
||||
pub bounds: ExportBounds,
|
||||
|
|
|
|||
|
|
@ -151,9 +151,11 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (file.name.endsWith(".graphite")) {
|
||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||
if (file.name.endsWith(graphiteFileSuffix)) {
|
||||
const content = await file.text();
|
||||
editor.handle.openDocumentFile(file.name, content);
|
||||
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
||||
editor.handle.openDocumentFile(documentName, content);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -438,9 +438,11 @@
|
|||
}
|
||||
|
||||
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
|
||||
if (file.name.endsWith(".graphite")) {
|
||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||
if (file.name.endsWith(graphiteFileSuffix)) {
|
||||
const content = await file.text();
|
||||
editor.handle.openDocumentFile(file.name, content);
|
||||
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
||||
editor.handle.openDocumentFile(documentName, content);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,9 +83,11 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (file.name.endsWith(".graphite")) {
|
||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||
if (file.name.endsWith(graphiteFileSuffix)) {
|
||||
const content = await file.text();
|
||||
editor.handle.openDocumentFile(file.name, content);
|
||||
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
||||
editor.handle.openDocumentFile(documentName, content);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -334,8 +334,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
|
||||
}
|
||||
|
||||
if (file.name.endsWith(".graphite")) {
|
||||
editor.handle.openDocumentFile(file.name, await file.text());
|
||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||
if (file.name.endsWith(graphiteFileSuffix)) {
|
||||
const content = await file.text();
|
||||
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
|
||||
editor.handle.openDocumentFile(documentName, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,9 +62,16 @@ export function createPortfolioState(editor: Editor) {
|
|||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
|
||||
const extension = editor.handle.fileSaveSuffix();
|
||||
const data = await upload(extension, "text");
|
||||
editor.handle.openDocumentFile(data.filename, data.content);
|
||||
const suffix = "." + editor.handle.fileExtension();
|
||||
const data = await upload(suffix, "text");
|
||||
|
||||
// Use filename as document name, removing the extension if it exists
|
||||
let documentName = data.filename;
|
||||
if (documentName.endsWith(suffix)) {
|
||||
documentName = documentName.slice(0, -suffix.length);
|
||||
}
|
||||
|
||||
editor.handle.openDocumentFile(documentName, data.content);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
||||
const data = await upload("image/*", "both");
|
||||
|
|
@ -76,8 +83,10 @@ export function createPortfolioState(editor: Editor) {
|
|||
}
|
||||
|
||||
// In case the user accidentally uploads a Graphite file, open it instead of failing to import it
|
||||
if (data.filename.endsWith(".graphite")) {
|
||||
editor.handle.openDocumentFile(data.filename, data.content.text);
|
||||
const graphiteFileSuffix = "." + editor.handle.fileExtension();
|
||||
if (data.filename.endsWith(graphiteFileSuffix)) {
|
||||
const documentName = data.filename.slice(0, -graphiteFileSuffix.length);
|
||||
editor.handle.openDocumentFile(documentName, data.content.text);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ export function downloadFile(filename: string, content: ArrayBuffer) {
|
|||
downloadFileBlob(filename, blob);
|
||||
}
|
||||
|
||||
export async function upload<T extends "text" | "data" | "both">(acceptedExtensions: string, textOrData: T): Promise<UploadResult<T>> {
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept for the `accept` string format
|
||||
export async function upload<T extends "text" | "data" | "both">(accept: string, textOrData: T): Promise<UploadResult<T>> {
|
||||
return new Promise<UploadResult<T>>((resolve, _) => {
|
||||
const element = document.createElement("input");
|
||||
element.type = "file";
|
||||
element.accept = acceptedExtensions;
|
||||
element.accept = accept;
|
||||
|
||||
element.addEventListener(
|
||||
"change",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
use crate::helpers::translate_key;
|
||||
use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER};
|
||||
use editor::consts::FILE_SAVE_SUFFIX;
|
||||
use editor::consts::FILE_EXTENSION;
|
||||
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
||||
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
|
||||
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
|
|
@ -344,10 +344,10 @@ impl EditorHandle {
|
|||
cfg!(debug_assertions)
|
||||
}
|
||||
|
||||
/// Get the constant `FILE_SAVE_SUFFIX`
|
||||
#[wasm_bindgen(js_name = fileSaveSuffix)]
|
||||
pub fn file_save_suffix(&self) -> String {
|
||||
FILE_SAVE_SUFFIX.into()
|
||||
/// Get the constant `FILE_EXTENSION`
|
||||
#[wasm_bindgen(js_name = fileExtension)]
|
||||
pub fn file_extension(&self) -> String {
|
||||
FILE_EXTENSION.into()
|
||||
}
|
||||
|
||||
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
|
||||
|
|
@ -421,7 +421,8 @@ impl EditorHandle {
|
|||
#[wasm_bindgen(js_name = openDocumentFile)]
|
||||
pub fn open_document_file(&self, document_name: String, document_serialized_content: String) {
|
||||
let message = PortfolioMessage::OpenDocumentFile {
|
||||
document_name,
|
||||
document_name: Some(document_name),
|
||||
document_path: None,
|
||||
document_serialized_content,
|
||||
};
|
||||
self.dispatch(message);
|
||||
|
|
@ -432,7 +433,8 @@ impl EditorHandle {
|
|||
let document_id = DocumentId(document_id);
|
||||
let message = PortfolioMessage::OpenDocumentFileWithId {
|
||||
document_id,
|
||||
document_name,
|
||||
document_name: Some(document_name),
|
||||
document_path: None,
|
||||
document_is_auto_saved: true,
|
||||
document_is_saved,
|
||||
document_serialized_content,
|
||||
|
|
|
|||
Loading…
Reference in New Issue