mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 20:58:12 +08:00
Implement IndexedDB document auto-save (#422)
* removed all use of document indicies * -add u64 support for wasm bridge * fixed rust formating * Cleaned up FrontendDocumentState in js-messages * Tiny tweaks from code review * - moved more of closeDocumentWithConfirmation to rust - updated serde_wasm_bindgen to add feature flag * working initial auto save impl * auto save is a lifetime file * - cargo fmt - fixc error message - move document version constant * code review round 1 * generate seed for uuid in js when wasm is initialized * Resolve PR feedback * Further address PR feedback * Fix failing test Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: otdavies <oliver@psyfer.io>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
otdavies
parent
ff39ebfdbb
commit
c9f140f458
@@ -96,7 +96,7 @@ impl Dispatcher {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{document::DocumentMessageHandler, message_prelude::*, misc::test_utils::EditorTestUtils, Editor};
|
use crate::{communication::set_uuid_seed, document::DocumentMessageHandler, message_prelude::*, misc::test_utils::EditorTestUtils, Editor};
|
||||||
use graphene::{color::Color, Operation};
|
use graphene::{color::Color, Operation};
|
||||||
|
|
||||||
fn init_logger() {
|
fn init_logger() {
|
||||||
@@ -108,6 +108,7 @@ mod test {
|
|||||||
/// 2. A blue shape
|
/// 2. A blue shape
|
||||||
/// 3. A green ellipse
|
/// 3. A green ellipse
|
||||||
fn create_editor_with_three_layers() -> Editor {
|
fn create_editor_with_three_layers() -> Editor {
|
||||||
|
set_uuid_seed(0);
|
||||||
let mut editor = Editor::new();
|
let mut editor = Editor::new();
|
||||||
|
|
||||||
editor.select_primary_color(Color::RED);
|
editor.select_primary_color(Color::RED);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use rand_chacha::{
|
|||||||
use spin::Mutex;
|
use spin::Mutex;
|
||||||
|
|
||||||
pub use crate::input::InputPreprocessor;
|
pub use crate::input::InputPreprocessor;
|
||||||
use std::collections::VecDeque;
|
use std::{cell::Cell, collections::VecDeque};
|
||||||
|
|
||||||
pub type ActionList = Vec<Vec<MessageDiscriminant>>;
|
pub type ActionList = Vec<Vec<MessageDiscriminant>>;
|
||||||
|
|
||||||
@@ -29,10 +29,21 @@ where
|
|||||||
fn actions(&self) -> ActionList;
|
fn actions(&self) -> ActionList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
pub static UUID_SEED: Cell<Option<u64>> = Cell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_uuid_seed(random_seed: u64) {
|
||||||
|
UUID_SEED.with(|seed| seed.set(Some(random_seed)))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_uuid() -> u64 {
|
pub fn generate_uuid() -> u64 {
|
||||||
let mut lock = RNG.lock();
|
let mut lock = RNG.lock();
|
||||||
if lock.is_none() {
|
if lock.is_none() {
|
||||||
*lock = Some(ChaCha20Rng::seed_from_u64(0));
|
UUID_SEED.with(|seed| {
|
||||||
|
let random_seed = seed.get().expect("random seed not set before editor was initialized");
|
||||||
|
*lock = Some(ChaCha20Rng::seed_from_u64(random_seed));
|
||||||
|
})
|
||||||
}
|
}
|
||||||
lock.as_mut().map(ChaCha20Rng::next_u64).unwrap()
|
lock.as_mut().map(ChaCha20Rng::next_u64).unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub use super::layer_panel::*;
|
|||||||
use super::movement_handler::{MovementMessage, MovementMessageHandler};
|
use super::movement_handler::{MovementMessage, MovementMessageHandler};
|
||||||
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};
|
use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler};
|
||||||
|
|
||||||
|
use crate::consts::DEFAULT_DOCUMENT_NAME;
|
||||||
use crate::consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING};
|
use crate::consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING};
|
||||||
use crate::input::InputPreprocessor;
|
use crate::input::InputPreprocessor;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
@@ -186,6 +187,13 @@ impl DocumentMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_unmodified_default(&self) -> bool {
|
||||||
|
self.serialize_root().len() == Self::default().serialize_root().len()
|
||||||
|
&& self.document_undo_history.len() == 0
|
||||||
|
&& self.document_redo_history.len() == 0
|
||||||
|
&& self.name.starts_with(DEFAULT_DOCUMENT_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
|
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
|
||||||
if self.graphene_document.layer(path).ok()?.overlay {
|
if self.graphene_document.layer(path).ok()?.overlay {
|
||||||
return None;
|
return None;
|
||||||
@@ -413,6 +421,14 @@ impl DocumentMessageHandler {
|
|||||||
self.current_identifier() == self.saved_document_identifier
|
self.current_identifier() == self.saved_document_identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_save_state(&mut self, is_saved: bool) {
|
||||||
|
if is_saved {
|
||||||
|
self.saved_document_identifier = self.current_identifier();
|
||||||
|
} else {
|
||||||
|
self.saved_document_identifier = generate_uuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
||||||
let data: LayerData = *layer_data(&mut self.layer_data, &path);
|
let data: LayerData = *layer_data(&mut self.layer_data, &path);
|
||||||
let layer = self.graphene_document.layer(&path)?;
|
let layer = self.graphene_document.layer(&path)?;
|
||||||
@@ -493,7 +509,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
SaveDocument => {
|
SaveDocument => {
|
||||||
self.saved_document_identifier = self.current_identifier();
|
self.set_save_state(true);
|
||||||
|
responses.push_back(DocumentsMessage::AutoSaveActiveDocument.into());
|
||||||
// Update the save status of the just saved document
|
// Update the save status of the just saved document
|
||||||
responses.push_back(DocumentsMessage::UpdateOpenDocumentsList.into());
|
responses.push_back(DocumentsMessage::UpdateOpenDocumentsList.into());
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,16 @@ pub enum DocumentsMessage {
|
|||||||
RequestAboutGraphiteDialog,
|
RequestAboutGraphiteDialog,
|
||||||
NewDocument,
|
NewDocument,
|
||||||
OpenDocument,
|
OpenDocument,
|
||||||
|
OpenDocumentFileWithId {
|
||||||
|
document: String,
|
||||||
|
document_name: String,
|
||||||
|
document_id: u64,
|
||||||
|
document_is_saved: bool,
|
||||||
|
},
|
||||||
OpenDocumentFile(String, String),
|
OpenDocumentFile(String, String),
|
||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
|
AutoSaveDocument(u64),
|
||||||
|
AutoSaveActiveDocument,
|
||||||
NextDocument,
|
NextDocument,
|
||||||
PrevDocument,
|
PrevDocument,
|
||||||
}
|
}
|
||||||
@@ -76,11 +84,23 @@ impl DocumentsMessageHandler {
|
|||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_document(&mut self, new_document: DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
// TODO Fix how this doesn't preserve tab order upon loading new document from file>load
|
||||||
let new_id = generate_uuid();
|
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, replace_first_empty: bool, responses: &mut VecDeque<Message>) {
|
||||||
self.active_document_id = new_id;
|
// Special case when loading a document on an empty page
|
||||||
self.document_ids.push(new_id);
|
if replace_first_empty && self.active_document().is_unmodified_default() {
|
||||||
self.documents.insert(new_id, new_document);
|
responses.push_back(DocumentsMessage::CloseDocument(self.active_document_id).into());
|
||||||
|
|
||||||
|
let active_document_index = self
|
||||||
|
.document_ids
|
||||||
|
.iter()
|
||||||
|
.position(|id| self.active_document_id == *id)
|
||||||
|
.expect("Did not find matching active document id");
|
||||||
|
self.document_ids.insert(active_document_index + 1, document_id);
|
||||||
|
} else {
|
||||||
|
self.document_ids.push(document_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.documents.insert(document_id, new_document);
|
||||||
|
|
||||||
// Send the new list of document tab names
|
// Send the new list of document tab names
|
||||||
let open_documents = self
|
let open_documents = self
|
||||||
@@ -97,12 +117,7 @@ impl DocumentsMessageHandler {
|
|||||||
|
|
||||||
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
||||||
|
|
||||||
responses.push_back(DocumentsMessage::SelectDocument(self.active_document_id).into());
|
responses.push_back(DocumentsMessage::SelectDocument(document_id).into());
|
||||||
responses.push_back(DocumentMessage::RenderDocument.into());
|
|
||||||
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
|
||||||
for layer in self.active_document().layer_data.keys() {
|
|
||||||
responses.push_back(DocumentMessage::LayerChanged(layer.clone()).into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns an iterator over the open documents in order
|
// Returns an iterator over the open documents in order
|
||||||
@@ -140,6 +155,10 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||||||
}
|
}
|
||||||
Document(message) => self.active_document_mut().process_action(message, ipp, responses),
|
Document(message) => self.active_document_mut().process_action(message, ipp, responses),
|
||||||
SelectDocument(id) => {
|
SelectDocument(id) => {
|
||||||
|
let active_document = self.active_document();
|
||||||
|
if !active_document.is_saved() {
|
||||||
|
responses.push_back(DocumentsMessage::AutoSaveDocument(self.active_document_id).into());
|
||||||
|
}
|
||||||
self.active_document_id = id;
|
self.active_document_id = id;
|
||||||
responses.push_back(FrontendMessage::SetActiveDocument { document_id: id }.into());
|
responses.push_back(FrontendMessage::SetActiveDocument { document_id: id }.into());
|
||||||
responses.push_back(RenderDocument.into());
|
responses.push_back(RenderDocument.into());
|
||||||
@@ -210,6 +229,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||||||
// Update the list of new documents on the front end, active tab, and ensure that document renders
|
// Update the list of new documents on the front end, active tab, and ensure that document renders
|
||||||
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
||||||
responses.push_back(FrontendMessage::SetActiveDocument { document_id: self.active_document_id }.into());
|
responses.push_back(FrontendMessage::SetActiveDocument { document_id: self.active_document_id }.into());
|
||||||
|
responses.push_back(FrontendMessage::RemoveAutoSaveDocument { document_id: id }.into());
|
||||||
responses.push_back(RenderDocument.into());
|
responses.push_back(RenderDocument.into());
|
||||||
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
responses.push_back(DocumentMessage::DocumentStructureChanged.into());
|
||||||
for layer in self.active_document().layer_data.keys() {
|
for layer in self.active_document().layer_data.keys() {
|
||||||
@@ -219,16 +239,33 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||||||
NewDocument => {
|
NewDocument => {
|
||||||
let name = self.generate_new_document_name();
|
let name = self.generate_new_document_name();
|
||||||
let new_document = DocumentMessageHandler::with_name(name, ipp);
|
let new_document = DocumentMessageHandler::with_name(name, ipp);
|
||||||
self.load_document(new_document, responses);
|
self.load_document(new_document, generate_uuid(), false, responses);
|
||||||
}
|
}
|
||||||
OpenDocument => {
|
OpenDocument => {
|
||||||
responses.push_back(FrontendMessage::OpenDocumentBrowse.into());
|
responses.push_back(FrontendMessage::OpenDocumentBrowse.into());
|
||||||
}
|
}
|
||||||
OpenDocumentFile(name, serialized_contents) => {
|
OpenDocumentFile(document_name, document) => {
|
||||||
let document = DocumentMessageHandler::with_name_and_content(name, serialized_contents, ipp);
|
responses.push_back(
|
||||||
|
DocumentsMessage::OpenDocumentFileWithId {
|
||||||
|
document,
|
||||||
|
document_name,
|
||||||
|
document_id: generate_uuid(),
|
||||||
|
document_is_saved: true,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OpenDocumentFileWithId {
|
||||||
|
document_name,
|
||||||
|
document_id,
|
||||||
|
document,
|
||||||
|
document_is_saved,
|
||||||
|
} => {
|
||||||
|
let document = DocumentMessageHandler::with_name_and_content(document_name, document, ipp);
|
||||||
match document {
|
match document {
|
||||||
Ok(document) => {
|
Ok(mut document) => {
|
||||||
self.load_document(document, responses);
|
document.set_save_state(document_is_saved);
|
||||||
|
self.load_document(document, document_id, true, responses);
|
||||||
}
|
}
|
||||||
Err(e) => responses.push_back(
|
Err(e) => responses.push_back(
|
||||||
FrontendMessage::DisplayError {
|
FrontendMessage::DisplayError {
|
||||||
@@ -254,18 +291,33 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
|
||||||
}
|
}
|
||||||
|
AutoSaveDocument(id) => {
|
||||||
|
let document = self.documents.get(&id).unwrap();
|
||||||
|
responses.push_back(
|
||||||
|
FrontendMessage::AutoSaveDocument {
|
||||||
|
document: document.graphene_document.serialize_document(),
|
||||||
|
details: FrontendDocumentDetails {
|
||||||
|
is_saved: document.is_saved(),
|
||||||
|
id,
|
||||||
|
name: document.name.clone(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AutoSaveActiveDocument => responses.push_back(DocumentsMessage::AutoSaveDocument(self.active_document_id).into()),
|
||||||
NextDocument => {
|
NextDocument => {
|
||||||
let current_index = self.document_index(self.active_document_id);
|
let current_index = self.document_index(self.active_document_id);
|
||||||
let next_index = (current_index + 1) % self.document_ids.len();
|
let next_index = (current_index + 1) % self.document_ids.len();
|
||||||
let next_id = self.document_ids[next_index];
|
let next_id = self.document_ids[next_index];
|
||||||
responses.push_back(SelectDocument(next_id).into());
|
responses.push_back(DocumentsMessage::SelectDocument(next_id).into());
|
||||||
}
|
}
|
||||||
PrevDocument => {
|
PrevDocument => {
|
||||||
let len = self.document_ids.len();
|
let len = self.document_ids.len();
|
||||||
let current_index = self.document_index(self.active_document_id);
|
let current_index = self.document_index(self.active_document_id);
|
||||||
let prev_index = (current_index + len - 1) % len;
|
let prev_index = (current_index + len - 1) % len;
|
||||||
let prev_id = self.document_ids[prev_index];
|
let prev_id = self.document_ids[prev_index];
|
||||||
responses.push_back(SelectDocument(prev_id).into());
|
responses.push_back(DocumentsMessage::SelectDocument(prev_id).into());
|
||||||
}
|
}
|
||||||
Copy => {
|
Copy => {
|
||||||
let paths = self.active_document().selected_layers_sorted();
|
let paths = self.active_document().selected_layers_sorted();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use crate::{
|
|||||||
input::{mouse::ViewportBounds, mouse::ViewportPosition, InputPreprocessor},
|
input::{mouse::ViewportBounds, mouse::ViewportPosition, InputPreprocessor},
|
||||||
};
|
};
|
||||||
use graphene::document::Document;
|
use graphene::document::Document;
|
||||||
use graphene::layers::style::ViewMode;
|
|
||||||
use graphene::Operation as DocumentOperation;
|
use graphene::Operation as DocumentOperation;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ pub enum FrontendMessage {
|
|||||||
UpdateRulers { origin: (f64, f64), spacing: f64, interval: f64 },
|
UpdateRulers { origin: (f64, f64), spacing: f64, interval: f64 },
|
||||||
ExportDocument { document: String, name: String },
|
ExportDocument { document: String, name: String },
|
||||||
SaveDocument { document: String, name: String },
|
SaveDocument { document: String, name: String },
|
||||||
|
AutoSaveDocument { document: String, details: FrontendDocumentDetails },
|
||||||
|
RemoveAutoSaveDocument { document_id: u64 },
|
||||||
OpenDocumentBrowse,
|
OpenDocumentBrowse,
|
||||||
UpdateWorkingColors { primary: Color, secondary: Color },
|
UpdateWorkingColors { primary: Color, secondary: Color },
|
||||||
SetCanvasZoom { new_zoom: f64 },
|
SetCanvasZoom { new_zoom: f64 },
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
extern crate graphite_proc_macros;
|
extern crate graphite_proc_macros;
|
||||||
|
|
||||||
mod communication;
|
pub mod communication;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
@@ -31,6 +31,8 @@ pub struct Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
|
/// Construct a new editor instance.
|
||||||
|
/// Remember to provide a random seed with `editor::communication::set_uuid_seed(seed)` before any editors can be used.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { dispatcher: Dispatcher::new() }
|
Self { dispatcher: Dispatcher::new() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ impl Fsm for EyedropperToolFsmState {
|
|||||||
self,
|
self,
|
||||||
event: ToolMessage,
|
event: ToolMessage,
|
||||||
document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
tool_data: &DocumentToolData,
|
_tool_data: &DocumentToolData,
|
||||||
data: &mut Self::ToolData,
|
_data: &mut Self::ToolData,
|
||||||
input: &InputPreprocessor,
|
input: &InputPreprocessor,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ impl Fsm for FillToolFsmState {
|
|||||||
event: ToolMessage,
|
event: ToolMessage,
|
||||||
document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
tool_data: &DocumentToolData,
|
tool_data: &DocumentToolData,
|
||||||
data: &mut Self::ToolData,
|
_data: &mut Self::ToolData,
|
||||||
input: &InputPreprocessor,
|
input: &InputPreprocessor,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|||||||
import { createEditorState, EditorState } from "@/state/wasm-loader";
|
import { createEditorState, EditorState } from "@/state/wasm-loader";
|
||||||
import { createInputManager, InputManager } from "@/lifetime/input";
|
import { createInputManager, InputManager } from "@/lifetime/input";
|
||||||
import { initErrorHandling } from "@/lifetime/errors";
|
import { initErrorHandling } from "@/lifetime/errors";
|
||||||
|
import { createAutoSaveManager } from "@/lifetime/auto-save";
|
||||||
|
|
||||||
// Vue injects don't play well with TypeScript, and all injects will show up as `any`. As a workaround, we can define these types.
|
// Vue injects don't play well with TypeScript, and all injects will show up as `any`. As a workaround, we can define these types.
|
||||||
declare module "@vue/runtime-core" {
|
declare module "@vue/runtime-core" {
|
||||||
@@ -259,6 +260,7 @@ export default defineComponent({
|
|||||||
const documents = createDocumentsState(editor, dialog);
|
const documents = createDocumentsState(editor, dialog);
|
||||||
const fullscreen = createFullscreenState();
|
const fullscreen = createFullscreenState();
|
||||||
initErrorHandling(editor, dialog);
|
initErrorHandling(editor, dialog);
|
||||||
|
createAutoSaveManager(editor, documents);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editor,
|
editor,
|
||||||
|
|||||||
@@ -19,18 +19,26 @@ export class JsMessage {
|
|||||||
// for details about how to transform the JSON from wasm-bindgen into classes.
|
// for details about how to transform the JSON from wasm-bindgen into classes.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export class FrontendDocumentDetails {
|
// Allows the auto save system to use a string for the id rather than a BigInt.
|
||||||
|
// IndexedDb does not allow for BigInts as primary keys. TypeScript does not allow
|
||||||
|
// subclasses to change the type of class variables in subclasses. It is an abstract
|
||||||
|
// class to point out that it should not be instantiated directly.
|
||||||
|
export abstract class DocumentDetails {
|
||||||
readonly name!: string;
|
readonly name!: string;
|
||||||
|
|
||||||
readonly is_saved!: boolean;
|
readonly is_saved!: boolean;
|
||||||
|
|
||||||
readonly id!: BigInt;
|
readonly id!: BigInt | string;
|
||||||
|
|
||||||
get displayName() {
|
get displayName() {
|
||||||
return `${this.name}${this.is_saved ? "" : "*"}`;
|
return `${this.name}${this.is_saved ? "" : "*"}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FrontendDocumentDetails extends DocumentDetails {
|
||||||
|
readonly id!: BigInt;
|
||||||
|
}
|
||||||
|
|
||||||
export class UpdateOpenDocumentsList extends JsMessage {
|
export class UpdateOpenDocumentsList extends JsMessage {
|
||||||
@Type(() => FrontendDocumentDetails)
|
@Type(() => FrontendDocumentDetails)
|
||||||
readonly open_documents!: FrontendDocumentDetails[];
|
readonly open_documents!: FrontendDocumentDetails[];
|
||||||
@@ -296,6 +304,24 @@ export const LayerTypeOptions = {
|
|||||||
|
|
||||||
export type LayerType = typeof LayerTypeOptions[keyof typeof LayerTypeOptions];
|
export type LayerType = typeof LayerTypeOptions[keyof typeof LayerTypeOptions];
|
||||||
|
|
||||||
|
export class IndexedDbDocumentDetails extends DocumentDetails {
|
||||||
|
@Transform(({ value }: { value: BigInt }) => value.toString())
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AutoSaveDocument extends JsMessage {
|
||||||
|
document!: string;
|
||||||
|
|
||||||
|
@Type(() => IndexedDbDocumentDetails)
|
||||||
|
details!: IndexedDbDocumentDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoveAutoSaveDocument extends JsMessage {
|
||||||
|
// Use a string since IndexedDB can not use BigInts for keys
|
||||||
|
@Transform(({ value }: { value: BigInt }) => value.toString())
|
||||||
|
document_id!: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Any is used since the type of the object should be known from the rust side
|
// Any is used since the type of the object should be known from the rust side
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage;
|
type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage;
|
||||||
@@ -322,5 +348,7 @@ export const messageConstructors: Record<string, MessageMaker> = {
|
|||||||
DisplayConfirmationToCloseDocument,
|
DisplayConfirmationToCloseDocument,
|
||||||
DisplayConfirmationToCloseAllDocuments,
|
DisplayConfirmationToCloseAllDocuments,
|
||||||
DisplayAboutGraphiteDialog,
|
DisplayAboutGraphiteDialog,
|
||||||
|
AutoSaveDocument,
|
||||||
|
RemoveAutoSaveDocument,
|
||||||
} as const;
|
} as const;
|
||||||
export type JsMessageType = keyof typeof messageConstructors;
|
export type JsMessageType = keyof typeof messageConstructors;
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { AutoSaveDocument, RemoveAutoSaveDocument } from "@/dispatcher/js-messages";
|
||||||
|
import { DocumentsState } from "@/state/documents";
|
||||||
|
import { EditorState } from "@/state/wasm-loader";
|
||||||
|
|
||||||
|
const GRAPHITE_INDEXED_DB_NAME = "graphite-indexed-db";
|
||||||
|
const GRAPHITE_INDEXED_DB_VERSION = 1;
|
||||||
|
const GRAPHITE_AUTO_SAVE_STORE = "auto-save-documents";
|
||||||
|
const GRAPHITE_AUTO_SAVE_ORDER_KEY = "auto-save-documents-order";
|
||||||
|
|
||||||
|
const databaseConnection: Promise<IDBDatabase> = new Promise((resolve) => {
|
||||||
|
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
|
||||||
|
|
||||||
|
dbOpenRequest.onupgradeneeded = () => {
|
||||||
|
const db = dbOpenRequest.result;
|
||||||
|
if (!db.objectStoreNames.contains(GRAPHITE_AUTO_SAVE_STORE)) {
|
||||||
|
db.createObjectStore(GRAPHITE_AUTO_SAVE_STORE, { keyPath: "details.id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dbOpenRequest.onerror = () => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Graphite IndexedDb error:", dbOpenRequest.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
dbOpenRequest.onsuccess = () => {
|
||||||
|
resolve(dbOpenRequest.result);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createAutoSaveManager(editor: EditorState, documents: DocumentsState) {
|
||||||
|
const openAutoSavedDocuments = async (): Promise<void> => {
|
||||||
|
const db = await databaseConnection;
|
||||||
|
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readonly");
|
||||||
|
const request = transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).getAll();
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const previouslySavedDocuments: AutoSaveDocument[] = request.result;
|
||||||
|
|
||||||
|
const documentOrder: string[] = JSON.parse(window.localStorage.getItem(GRAPHITE_AUTO_SAVE_ORDER_KEY) || "[]");
|
||||||
|
const orderedSavedDocuments = documentOrder.map((id) => previouslySavedDocuments.find((autoSave) => autoSave.details.id === id)).filter((x) => x !== undefined) as AutoSaveDocument[];
|
||||||
|
|
||||||
|
orderedSavedDocuments.forEach((doc: AutoSaveDocument) => {
|
||||||
|
editor.instance.open_auto_saved_document(BigInt(doc.details.id), doc.details.name, doc.details.is_saved, doc.document);
|
||||||
|
});
|
||||||
|
resolve(undefined);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeDocumentOrder = () => {
|
||||||
|
// Make sure to store as string since JSON does not play nice with BigInt
|
||||||
|
const documentOrder = documents.state.documents.map((doc) => doc.id.toString());
|
||||||
|
window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder));
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.dispatcher.subscribeJsMessage(AutoSaveDocument, async (autoSaveDocument) => {
|
||||||
|
const db = await databaseConnection;
|
||||||
|
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite");
|
||||||
|
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).put(autoSaveDocument);
|
||||||
|
storeDocumentOrder();
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.dispatcher.subscribeJsMessage(RemoveAutoSaveDocument, async (removeAutoSaveDocument) => {
|
||||||
|
const db = await databaseConnection;
|
||||||
|
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite");
|
||||||
|
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).delete(removeAutoSaveDocument.document_id);
|
||||||
|
storeDocumentOrder();
|
||||||
|
});
|
||||||
|
|
||||||
|
// On creation
|
||||||
|
openAutoSavedDocuments();
|
||||||
|
|
||||||
|
return {
|
||||||
|
openAutoSavedDocuments,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -169,6 +169,9 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onBeforeUnload = (e: BeforeUnloadEvent) => {
|
const onBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||||
|
const activeDocument = document.state.documents[document.state.activeDocumentIndex];
|
||||||
|
if (!activeDocument.is_saved) editor.instance.trigger_auto_save(activeDocument.id);
|
||||||
|
|
||||||
// Skip the message if the editor crashed, since work is already lost
|
// Skip the message if the editor crashed, since work is already lost
|
||||||
if (editor.instance.has_crashed()) return;
|
if (editor.instance.has_crashed()) return;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ let wasmImport: WasmInstance | null = null;
|
|||||||
export async function initWasm() {
|
export async function initWasm() {
|
||||||
if (wasmImport !== null) return;
|
if (wasmImport !== null) return;
|
||||||
|
|
||||||
wasmImport = await import("@/../wasm/pkg").then(panicProxy);
|
// Separating in two lines satisfies typescript when used below
|
||||||
|
const importedWasm = await import("@/../wasm/pkg").then(panicProxy);
|
||||||
|
wasmImport = importedWasm;
|
||||||
|
|
||||||
|
const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
|
||||||
|
importedWasm.set_random_seed(randomSeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This works by proxying every function call wrapping a try-catch block to filter out redundant and confusing
|
// This works by proxying every function call wrapping a try-catch block to filter out redundant and confusing
|
||||||
|
|||||||
@@ -175,11 +175,26 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_auto_saved_document(&self, document_id: u64, document_name: String, document_is_saved: bool, document: String) {
|
||||||
|
let message = DocumentsMessage::OpenDocumentFileWithId {
|
||||||
|
document_id,
|
||||||
|
document_name,
|
||||||
|
document_is_saved,
|
||||||
|
document,
|
||||||
|
};
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn save_document(&self) {
|
pub fn save_document(&self) {
|
||||||
let message = DocumentMessage::SaveDocument;
|
let message = DocumentMessage::SaveDocument;
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn trigger_auto_save(&self, document_id: u64) {
|
||||||
|
let message = DocumentsMessage::AutoSaveDocument(document_id);
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn close_document(&self, document_id: u64) {
|
pub fn close_document(&self, document_id: u64) {
|
||||||
let message = DocumentsMessage::CloseDocument(document_id);
|
let message = DocumentsMessage::CloseDocument(document_id);
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
@@ -509,3 +524,8 @@ pub fn i32_max() -> i32 {
|
|||||||
pub fn i32_min() -> i32 {
|
pub fn i32_min() -> i32 {
|
||||||
i32::MIN
|
i32::MIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn set_random_seed(seed: u64) {
|
||||||
|
editor::communication::set_uuid_seed(seed)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::color::Color;
|
use crate::color::Color;
|
||||||
|
|
||||||
|
// Document
|
||||||
|
pub const GRAPHENE_DOCUMENT_VERSION: &'static str = "0.0.1";
|
||||||
|
|
||||||
// RENDERING
|
// RENDERING
|
||||||
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
|
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
|
||||||
pub const LAYER_OUTLINE_STROKE_WIDTH: f32 = 1.;
|
pub const LAYER_OUTLINE_STROKE_WIDTH: f32 = 1.;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::consts::GRAPHENE_DOCUMENT_VERSION;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::max,
|
cmp::max,
|
||||||
collections::hash_map::DefaultHasher,
|
collections::hash_map::DefaultHasher,
|
||||||
@@ -19,6 +20,7 @@ pub struct Document {
|
|||||||
/// This identifier is not a hash and is not guaranteed to be equal for equivalent documents.
|
/// This identifier is not a hash and is not guaranteed to be equal for equivalent documents.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub state_identifier: DefaultHasher,
|
pub state_identifier: DefaultHasher,
|
||||||
|
pub graphene_document_version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Document {
|
impl Default for Document {
|
||||||
@@ -26,6 +28,7 @@ impl Default for Document {
|
|||||||
Self {
|
Self {
|
||||||
root: Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()),
|
root: Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()),
|
||||||
state_identifier: DefaultHasher::new(),
|
state_identifier: DefaultHasher::new(),
|
||||||
|
graphene_document_version: GRAPHENE_DOCUMENT_VERSION.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user