diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 4a63e1a8..bbba55c5 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,4 +1,4 @@ -use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon}; +use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ @@ -90,13 +90,15 @@ pub enum FrontendMessage { font: Font, }, TriggerImport, - TriggerIndexedDbRemoveDocument { + TriggerPersistenceRemoveDocument { #[serde(rename = "documentId")] document_id: DocumentId, }, - TriggerIndexedDbWriteDocument { + TriggerPersistenceWriteDocument { + #[serde(rename = "documentId")] + document_id: DocumentId, document: String, - details: FrontendDocumentDetails, + details: DocumentDetails, }, TriggerLoadFirstAutoSaveDocument, TriggerLoadRestAutoSaveDocuments, @@ -308,7 +310,7 @@ pub enum FrontendMessage { }, UpdateOpenDocumentsList { #[serde(rename = "openDocuments")] - open_documents: Vec, + open_documents: Vec, }, UpdatePropertiesPanelLayout { #[serde(rename = "layoutTarget")] diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index cb55047d..7f987340 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -2,13 +2,18 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::prelude::*; #[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct FrontendDocumentDetails { - #[serde(rename = "isAutoSaved")] - pub is_auto_saved: bool, +pub struct OpenDocument { + pub id: DocumentId, + pub details: DocumentDetails, +} + +#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct DocumentDetails { + pub name: String, #[serde(rename = "isSaved")] pub is_saved: bool, - pub name: String, - pub id: DocumentId, + #[serde(rename = "isAutoSaved")] + pub is_auto_saved: bool, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index aec19541..87e3c6ce 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -952,6 +952,7 @@ impl MessageHandler> for DocumentMes self.path = None; self.set_save_state(false); + self.set_auto_save_state(false); responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::UpdateNewNodeGraph); @@ -1301,6 +1302,7 @@ impl MessageHandler> for DocumentMes } self.network_interface.finish_transaction(); self.document_redo_history.clear(); + responses.add(PortfolioMessage::UpdateOpenDocumentsList); } DocumentMessage::AbortTransaction => { responses.add(DocumentMessage::RepeatedAbortTransaction { undo_count: 1 }); @@ -1316,6 +1318,7 @@ impl MessageHandler> for DocumentMes self.network_interface.finish_transaction(); responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateOpenDocumentsList); } DocumentMessage::ToggleLayerExpansion { id, recursive } => { let layer = LayerNodeIdentifier::new(id, &self.network_interface); @@ -1975,16 +1978,16 @@ impl DocumentMessageHandler { Some(previous_network) } - pub fn current_hash(&self) -> Option { - self.document_undo_history.iter().last().map(|network| network.document_network().current_hash()) + pub fn current_hash(&self) -> u64 { + self.network_interface.document_network().current_hash() } pub fn is_auto_saved(&self) -> bool { - self.current_hash() == self.auto_saved_hash + Some(self.current_hash()) == self.auto_saved_hash } pub fn is_saved(&self) -> bool { - self.current_hash() == self.saved_hash + Some(self.current_hash()) == self.saved_hash } pub fn is_graph_overlay_open(&self) -> bool { @@ -1993,7 +1996,7 @@ impl DocumentMessageHandler { pub fn set_auto_save_state(&mut self, is_saved: bool) { if is_saved { - self.auto_saved_hash = self.current_hash(); + self.auto_saved_hash = Some(self.current_hash()); } else { self.auto_saved_hash = None; } @@ -2001,7 +2004,7 @@ impl DocumentMessageHandler { pub fn set_save_state(&mut self, is_saved: bool) { if is_saved { - self.saved_hash = self.current_hash(); + self.saved_hash = Some(self.current_hash()); } else { self.saved_hash = None; } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index f11cc394..5fd1a898 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -6,7 +6,7 @@ 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; -use crate::messages::frontend::utility_types::FrontendDocumentDetails; +use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument}; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::DocumentMessageContext; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; @@ -187,13 +187,13 @@ impl MessageHandler> for Portfolio } PortfolioMessage::AutoSaveDocument { document_id } => { let document = self.documents.get(&document_id).unwrap(); - responses.add(FrontendMessage::TriggerIndexedDbWriteDocument { + responses.add(FrontendMessage::TriggerPersistenceWriteDocument { + document_id, document: document.serialize_document(), - details: FrontendDocumentDetails { - is_auto_saved: document.is_auto_saved(), - is_saved: document.is_saved(), - id: document_id, + details: DocumentDetails { name: document.name.clone(), + is_saved: document.is_saved(), + is_auto_saved: document.is_auto_saved(), }, }) } @@ -216,7 +216,7 @@ impl MessageHandler> for Portfolio } for document_id in &self.document_ids { - responses.add(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id: *document_id }); + responses.add(FrontendMessage::TriggerPersistenceRemoveDocument { document_id: *document_id }); } responses.add(PortfolioMessage::DestroyAllDocuments); @@ -242,7 +242,7 @@ impl MessageHandler> for Portfolio // Actually delete the document (delay to delete document is required to let the document and properties panel messages above get processed) responses.add(PortfolioMessage::DeleteDocument { document_id }); - responses.add(FrontendMessage::TriggerIndexedDbRemoveDocument { document_id }); + responses.add(FrontendMessage::TriggerPersistenceRemoveDocument { document_id }); // Send the new list of document tab names responses.add(PortfolioMessage::UpdateOpenDocumentsList); @@ -1044,11 +1044,13 @@ impl MessageHandler> for Portfolio .document_ids .iter() .filter_map(|id| { - self.documents.get(id).map(|document| FrontendDocumentDetails { - is_auto_saved: document.is_auto_saved(), - is_saved: document.is_saved(), + self.documents.get(id).map(|document| OpenDocument { id: *id, - name: document.name.clone(), + details: DocumentDetails { + is_auto_saved: document.is_auto_saved(), + is_saved: document.is_saved(), + name: document.name.clone(), + }, }) }) .collect::>(); diff --git a/frontend/src/components/window/workspace/Workspace.svelte b/frontend/src/components/window/workspace/Workspace.svelte index 89607844..e9456e92 100644 --- a/frontend/src/components/window/workspace/Workspace.svelte +++ b/frontend/src/components/window/workspace/Workspace.svelte @@ -2,7 +2,7 @@ import { getContext } from "svelte"; import type { Editor } from "@graphite/editor"; - import type { FrontendDocumentDetails } from "@graphite/messages"; + import type { OpenDocument } from "@graphite/messages"; import type { DialogState } from "@graphite/state-providers/dialog"; import type { PortfolioState } from "@graphite/state-providers/portfolio"; @@ -29,7 +29,7 @@ $: documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex); - $: documentTabLabels = $portfolio.documents.map((doc: FrontendDocumentDetails) => { + $: documentTabLabels = $portfolio.documents.map((doc: OpenDocument) => { const name = doc.displayName; if (!editor.handle.inDevelopmentMode()) return { name }; diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index f0ac34db..6da2dddc 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -283,7 +283,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli async function onBeforeUnload(e: BeforeUnloadEvent) { const activeDocument = get(portfolio).documents[get(portfolio).activeDocumentIndex]; - if (activeDocument && !activeDocument.isAutoSaved) editor.handle.triggerAutoSave(activeDocument.id); + if (activeDocument && !activeDocument.details.isAutoSaved) editor.handle.triggerAutoSave(activeDocument.id); // Skip the message if the editor crashed, since work is already lost if (await editor.handle.hasCrashed()) return; @@ -291,7 +291,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Skip the message during development, since it's annoying when testing if (await editor.handle.inDevelopmentMode()) return; - const allDocumentsSaved = get(portfolio).documents.reduce((acc, doc) => acc && doc.isSaved, true); + const allDocumentsSaved = get(portfolio).documents.reduce((acc, doc) => acc && doc.details.isSaved, true); if (!allDocumentsSaved) { e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?"; e.preventDefault(); diff --git a/frontend/src/io-managers/persistence.ts b/frontend/src/io-managers/persistence.ts index 2cdb3829..44af91d3 100644 --- a/frontend/src/io-managers/persistence.ts +++ b/frontend/src/io-managers/persistence.ts @@ -3,8 +3,8 @@ import { get as getFromStore } from "svelte/store"; import { type Editor } from "@graphite/editor"; import { - TriggerIndexedDbWriteDocument, - TriggerIndexedDbRemoveDocument, + TriggerPersistenceWriteDocument, + TriggerPersistenceRemoveDocument, TriggerSavePreferences, TriggerLoadPreferences, TriggerLoadFirstAutoSaveDocument, @@ -27,23 +27,23 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta await set("current_document_id", String(documentId), graphiteStore); } - async function storeDocument(autoSaveDocument: TriggerIndexedDbWriteDocument) { - await update>( + async function storeDocument(autoSaveDocument: TriggerPersistenceWriteDocument) { + await update>( "documents", (old) => { const documents = old || {}; - documents[autoSaveDocument.details.id] = autoSaveDocument; + documents[autoSaveDocument.documentId] = autoSaveDocument; return documents; }, graphiteStore, ); await storeDocumentOrder(); - await storeCurrentDocumentId(autoSaveDocument.details.id); + await storeCurrentDocumentId(autoSaveDocument.documentId); } async function removeDocument(id: string) { - await update>( + await update>( "documents", (old) => { const documents = old || {}; @@ -77,7 +77,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta } async function loadFirstDocument() { - const previouslySavedDocuments = await get>("documents", graphiteStore); + const previouslySavedDocuments = await get>("documents", graphiteStore); const documentOrder = await get("documents_tab_order", graphiteStore); const currentDocumentId = await get("current_document_id", graphiteStore); if (!previouslySavedDocuments || !documentOrder) return; @@ -86,20 +86,20 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta if (currentDocumentId && currentDocumentId in previouslySavedDocuments) { const doc = previouslySavedDocuments[currentDocumentId]; - editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document, false); + editor.handle.openAutoSavedDocument(BigInt(doc.documentId), doc.details.name, doc.details.isSaved, doc.document, false); editor.handle.selectDocument(BigInt(currentDocumentId)); } else { const len = orderedSavedDocuments.length; if (len > 0) { const doc = orderedSavedDocuments[len - 1]; - editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document, false); - editor.handle.selectDocument(BigInt(doc.details.id)); + editor.handle.openAutoSavedDocument(BigInt(doc.documentId), doc.details.name, doc.details.isSaved, doc.document, false); + editor.handle.selectDocument(BigInt(doc.documentId)); } } } async function loadRestDocuments() { - const previouslySavedDocuments = await get>("documents", graphiteStore); + const previouslySavedDocuments = await get>("documents", graphiteStore); const documentOrder = await get("documents_tab_order", graphiteStore); const currentDocumentId = await get("current_document_id", graphiteStore); if (!previouslySavedDocuments || !documentOrder) return; @@ -107,19 +107,19 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); if (currentDocumentId) { - const currentIndex = orderedSavedDocuments.findIndex((doc) => doc.details.id === currentDocumentId); + const currentIndex = orderedSavedDocuments.findIndex((doc) => doc.documentId === currentDocumentId); const beforeCurrentIndex = currentIndex - 1; const afterCurrentIndex = currentIndex + 1; for (let i = beforeCurrentIndex; i >= 0; i--) { - const { document, details } = orderedSavedDocuments[i]; - const { id, name, isSaved } = details; - editor.handle.openAutoSavedDocument(BigInt(id), name, isSaved, document, true); + const { documentId, document, details } = orderedSavedDocuments[i]; + const { name, isSaved } = details; + editor.handle.openAutoSavedDocument(BigInt(documentId), name, isSaved, document, true); } for (let i = afterCurrentIndex; i < orderedSavedDocuments.length; i++) { - const { document, details } = orderedSavedDocuments[i]; - const { id, name, isSaved } = details; - editor.handle.openAutoSavedDocument(BigInt(id), name, isSaved, document, false); + const { documentId, document, details } = orderedSavedDocuments[i]; + const { name, isSaved } = details; + editor.handle.openAutoSavedDocument(BigInt(documentId), name, isSaved, document, false); } editor.handle.selectDocument(BigInt(currentDocumentId)); @@ -127,13 +127,13 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta const length = orderedSavedDocuments.length; for (let i = length - 2; i >= 0; i--) { - const { document, details } = orderedSavedDocuments[i]; - const { id, name, isSaved } = details; - editor.handle.openAutoSavedDocument(BigInt(id), name, isSaved, document, true); + const { documentId, document, details } = orderedSavedDocuments[i]; + const { name, isSaved } = details; + editor.handle.openAutoSavedDocument(BigInt(documentId), name, isSaved, document, true); } if (length > 0) { - const id = orderedSavedDocuments[length - 1].details.id; + const id = orderedSavedDocuments[length - 1].documentId; editor.handle.selectDocument(BigInt(id)); } } @@ -161,10 +161,10 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta editor.subscriptions.subscribeJsMessage(TriggerLoadPreferences, async () => { await loadPreferences(); }); - editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => { + editor.subscriptions.subscribeJsMessage(TriggerPersistenceWriteDocument, async (autoSaveDocument) => { await storeDocument(autoSaveDocument); }); - editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => { + editor.subscriptions.subscribeJsMessage(TriggerPersistenceRemoveDocument, async (removeAutoSaveDocument) => { await removeDocument(removeAutoSaveDocument.documentId); }); editor.subscriptions.subscribeJsMessage(TriggerLoadFirstAutoSaveDocument, async () => { @@ -175,7 +175,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta }); editor.subscriptions.subscribeJsMessage(TriggerSaveActiveDocument, async (triggerSaveActiveDocument) => { const documentId = String(triggerSaveActiveDocument.documentId); - const previouslySavedDocuments = await get>("documents", graphiteStore); + const previouslySavedDocuments = await get>("documents", graphiteStore); if (!previouslySavedDocuments) return; if (documentId in previouslySavedDocuments) { await storeCurrentDocumentId(documentId); diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index c47c520f..83234a04 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -129,37 +129,36 @@ export class UpdateNodeGraphSelection extends JsMessage { } export class UpdateOpenDocumentsList extends JsMessage { - @Type(() => FrontendDocumentDetails) - readonly openDocuments!: FrontendDocumentDetails[]; + @Type(() => OpenDocument) + readonly openDocuments!: OpenDocument[]; } export class UpdateWirePathInProgress extends JsMessage { readonly wirePath!: WirePath | undefined; } -// 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 { +export class OpenDocument { + readonly id!: bigint; + @Type(() => DocumentDetails) + readonly details!: DocumentDetails; + + get displayName(): string { + return this.details.displayName; + } +} + +export class DocumentDetails { readonly name!: string; readonly isAutoSaved!: boolean; readonly isSaved!: boolean; - // This field must be provided by the subclass implementation - // readonly id!: bigint | string; - get displayName(): string { return `${this.name}${this.isSaved ? "" : "*"}`; } } -export class FrontendDocumentDetails extends DocumentDetails { - readonly id!: bigint; -} - export class Box { readonly startX!: number; @@ -277,21 +276,20 @@ export class WireUpdate { readonly wirePathUpdate!: WirePath | undefined; } -export class IndexedDbDocumentDetails extends DocumentDetails { +export class TriggerPersistenceWriteDocument extends JsMessage { + // Use a string since IndexedDB can not use BigInts for keys @Transform(({ value }: { value: bigint }) => value.toString()) - id!: string; -} + documentId!: string; -export class TriggerIndexedDbWriteDocument extends JsMessage { document!: string; - @Type(() => IndexedDbDocumentDetails) - details!: IndexedDbDocumentDetails; + @Type(() => DocumentDetails) + details!: DocumentDetails; version!: string; } -export class TriggerIndexedDbRemoveDocument extends JsMessage { +export class TriggerPersistenceRemoveDocument extends JsMessage { // Use a string since IndexedDB can not use BigInts for keys @Transform(({ value }: { value: bigint }) => value.toString()) documentId!: string; @@ -1643,8 +1641,8 @@ export const messageMakers: Record = { TriggerFetchAndOpenDocument, TriggerFontLoad, TriggerImport, - TriggerIndexedDbRemoveDocument, - TriggerIndexedDbWriteDocument, + TriggerPersistenceRemoveDocument, + TriggerPersistenceWriteDocument, TriggerLoadFirstAutoSaveDocument, TriggerLoadPreferences, TriggerLoadRestAutoSaveDocuments, diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 6259ce7c..4b55a8e6 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -1,8 +1,8 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; +import type { OpenDocument } from "@graphite/messages"; import { - type FrontendDocumentDetails, TriggerFetchAndOpenDocument, TriggerSaveDocument, TriggerExportImage, @@ -21,7 +21,7 @@ import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rast export function createPortfolioState(editor: Editor) { const { subscribe, update } = writable({ unsaved: false, - documents: [] as FrontendDocumentDetails[], + documents: [] as OpenDocument[], activeDocumentIndex: 0, dataPanelOpen: false, propertiesPanelOpen: true,