diff --git a/editor/graphite-test-document.graphite b/editor/graphite-test-document.graphite index f7eba7cb..ecdc9aff 100644 --- a/editor/graphite-test-document.graphite +++ b/editor/graphite-test-document.graphite @@ -1 +1 @@ -{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":10689566813179949075,"layer_ids":[10689566813179949074],"layers":[{"visible":true,"name":"Folder 1","data":{"Folder":{"next_assignment_id":17222868332373661780,"layer_ids":[17222868332373661779],"layers":[{"visible":true,"name":"Shape 1","data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.5,1.0],"manipulator_type":"Anchor"},{"position":[0.7761415,1.0],"manipulator_type":"InHandle"},{"position":[0.22385850000000002,1.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.0,0.5],"manipulator_type":"Anchor"},{"position":[0.0,0.7761415],"manipulator_type":"InHandle"},{"position":[0.0,0.22385850000000002],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.5,0.0],"manipulator_type":"Anchor"},{"position":[0.22385850000000002,0.0],"manipulator_type":"InHandle"},{"position":[0.7761415,0.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},{"position":[1.0,0.22385850000000002],"manipulator_type":"InHandle"},{"position":[1.0,0.7761415],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5],"next_id":6},"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[379.0,0.0,-0.0,239.0],"translation":[-479.3046875,-99.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":6520881531418194372,"name":"Untitled Document","version":"0.0.14","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[],{"selected":false,"expanded":true}],[[10689566813179949074],{"selected":false,"expanded":true}],[[10689566813179949074,17222868332373661779],{"selected":true,"expanded":false}]],"layer_range_selection_reference":[10689566813179949074,17222868332373661779],"navigation_handler":{"pan":[0.0,0.0],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[10689566813179949074,17222868332373661779],"Artwork"]}} +{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":15997858604048146086,"layer_ids":[17969178651777368768,3643066017826641933,16075447627426792792,10598788542653937530,6240415495114589106,5260049129070821769,6431150803620977111,13779825621578452744,3547353463246861192],"layers":[{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[201.95915125256778,132.20589235454912],"manipulator_type":"Anchor"},{"position":[201.95915125256778,132.20589235454912],"manipulator_type":"InHandle"},{"position":[201.95915125256778,132.20589235454912],"manipulator_type":"OutHandle"}]},{"points":[{"position":[347.5538879474682,92.26397343824904],"manipulator_type":"Anchor"},{"position":[347.5538879474682,51.03360552464895],"manipulator_type":"InHandle"},{"position":[347.5538879474682,133.49434135184913],"manipulator_type":"OutHandle"}]},{"points":[{"position":[283.131438082468,237.85871013314937],"manipulator_type":"Anchor"},{"position":[337.2462959690681,281.6659760413495],"manipulator_type":"InHandle"},{"position":[229.01658019586787,194.05144422494925],"manipulator_type":"OutHandle"}]},{"points":[{"position":[154.28653835246766,246.87785311424938],"manipulator_type":"Anchor"},{"position":[154.28653835246766,246.87785311424938],"manipulator_type":"InHandle"},{"position":[154.28653835246766,246.87785311424938],"manipulator_type":"OutHandle"}]}],"element_ids":[1,2,3,4],"next_id":5},"style":{"stroke":{"color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":"None"},"render_index":1}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[433.87997076656836,163.73088039119517],"manipulator_type":"Anchor"},{"position":[433.87997076656836,163.73088039119517],"manipulator_type":"InHandle"},{"position":[433.87997076656836,163.73088039119517],"manipulator_type":"OutHandle"}]},{"points":[{"position":[592.3591974344687,156.68642330324917],"manipulator_type":"Anchor"},{"position":[592.3591974344687,156.68642330324917],"manipulator_type":"InHandle"},{"position":[592.3591974344687,156.68642330324917],"manipulator_type":"OutHandle"}]},{"points":[{"position":[463.51429770446845,311.3003029792495],"manipulator_type":"Anchor"},{"position":[463.51429770446845,311.3003029792495],"manipulator_type":"InHandle"},{"position":[463.51429770446845,311.3003029792495],"manipulator_type":"OutHandle"}]}],"element_ids":[1,2,3],"next_id":4},"style":{"stroke":{"color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":"None"},"render_index":1}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[801.087934997069,163.7308803911952],"manipulator_type":"Anchor"},{"position":[801.087934997069,163.7308803911952],"manipulator_type":"InHandle"},{"position":[801.087934997069,163.7308803911952],"manipulator_type":"OutHandle"}]},{"points":[{"position":[767.5882610672691,295.8389150116495],"manipulator_type":"Anchor"},{"position":[767.5882610672691,295.8389150116495],"manipulator_type":"InHandle"},{"position":[767.5882610672691,295.8389150116495],"manipulator_type":"OutHandle"}]},{"points":[{"position":[652.9163003075688,233.99336314124935],"manipulator_type":"Anchor"},{"position":[652.9163003075688,233.99336314124935],"manipulator_type":"InHandle"},{"position":[652.9163003075688,233.99336314124935],"manipulator_type":"OutHandle"}]},{"points":[{"position":[735.377036134769,74.22568747604898],"manipulator_type":"Anchor"},{"position":[735.377036134769,74.22568747604898],"manipulator_type":"InHandle"},{"position":[735.377036134769,74.22568747604898],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,6],"next_id":6},"style":{"stroke":{"color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":"None"},"render_index":1}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[106.61392545236757,387.3187938199497],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[107.90237444966755,387.3187938199497],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[125.94066041186761,374.43430384694966],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[140.11359938216765,364.12671186854965],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[152.99808935516768,355.10756888744964],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[167.17102832546772,348.6653239009496],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[177.47862030386773,347.3768749036496],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[189.07466127956775,357.68446688204966],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[198.09380426066778,374.43430384694966],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[210.9782942336678,402.78018178754974],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[222.57433520936783,428.5491617335498],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[232.88192718776784,434.99140672004984],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[244.47796816346786,437.56830471464986],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[274.112295101368,431.1260597281498],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[324.3618059960681,402.78018178754974],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[361.7268269177682,373.1458548496497],"manipulator_type":"Anchor"},null,null]}],"element_ids":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"next_id":16},"style":{"stroke":{"color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":{"Gradient":{"start":[0.6212121212121211,0.9857142857142858],"end":[0.6212121212121211,0.9857142857142858],"transform":[198.00000000000006,0.0,0.0,70.00000000000004,133.0,328.0],"positions":[[0.0,{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}],[1.0,{"red":0.7647059,"green":0.2627451,"blue":0.5921569,"alpha":1.0}]],"uuid":2843271626210251322,"gradient_type":"Linear"}}},"render_index":0}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[37.365020921700086,6.442244986499993]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[643.8971573264688,410.5108757713498],"manipulator_type":"Anchor"},null,{"position":[594.5066124299686,427.50613159287843],"manipulator_type":"OutHandle"}]},{"points":[{"position":[553.7057275154685,420.8184677497498],"manipulator_type":"Anchor"},{"position":[545.1160675334686,444.501387414407],"manipulator_type":"InHandle"},{"position":[562.2953874974685,397.1355480850926],"manipulator_type":"OutHandle"}]},{"points":[{"position":[673.5314842643688,339.6461809198496],"manipulator_type":"Anchor"},{"position":[628.8652523579686,332.7744529342495],"manipulator_type":"InHandle"},{"position":[718.197716170769,346.5179089054497],"manipulator_type":"OutHandle"}]},{"points":[{"position":[766.2998120699691,438.85675371194986],"manipulator_type":"Anchor"},{"position":[740.960315123069,424.62246002749276],"manipulator_type":"InHandle"},{"position":[791.6393090168692,453.09104739640696],"manipulator_type":"OutHandle"}]},{"points":[{"position":[847.4720988998693,353.81911989014964],"manipulator_type":"Anchor"},{"position":[819.5557039583692,403.4550836432783],"manipulator_type":"InHandle"},null]}],"element_ids":[1,2,3,4,5],"next_id":5},"style":{"stroke":{"color":{"red":0.6627451,"green":0.003921569,"blue":0.34509805,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":"None"},"render_index":0}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.0,0.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[1.0,0.0],"manipulator_type":"Anchor"},null,null]}],"element_ids":[1,2],"next_id":2},"style":{"stroke":{"color":{"red":0.29803923,"green":0.14901961,"blue":0.91764706,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":"None"},"render_index":1}},"transform":{"matrix2":[-79.8838378326002,220.22380660417988,-1.211224462609144,-0.439358759808626],"translation":[1008.5282235623696,221.10887316824932]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.0,0.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[0.0,1.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[1.0,1.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[1.0,0.0],"manipulator_type":"Anchor"},null,null]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5],"next_id":5},"style":{"stroke":null,"fill":{"Gradient":{"start":[0.050724637681159424,0.3625000000000007],"end":[0.9492753623188404,0.6375000000000002],"transform":[138.0,0.0,0.0,80.0,183.0,455.0],"positions":[[0.0,{"red":0.21568628,"green":0.61960787,"blue":0.015686275,"alpha":1.0}],[1.0,{"red":0.03137255,"green":0.16470589,"blue":0.22352941,"alpha":1.0}]],"uuid":16297296248403562943,"gradient_type":"Linear"}}},"render_index":1}},"transform":{"matrix2":[177.80596162740042,0.0,-0.0,103.07591978400025],"translation":[208.4013962390678,517.45214254725]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.5,1.0],"manipulator_type":"Anchor"},{"position":[0.7761415,1.0],"manipulator_type":"InHandle"},{"position":[0.22385850000000002,1.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.0,0.5],"manipulator_type":"Anchor"},{"position":[0.0,0.7761415],"manipulator_type":"InHandle"},{"position":[0.0,0.22385850000000002],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.5,0.0],"manipulator_type":"Anchor"},{"position":[0.22385850000000002,0.0],"manipulator_type":"InHandle"},{"position":[0.7761415,0.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},{"position":[1.0,0.22385850000000002],"manipulator_type":"InHandle"},{"position":[1.0,0.7761415],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5],"next_id":5},"style":{"stroke":null,"fill":{"Solid":{"red":0.8039216,"green":0.2784314,"blue":0.29803923,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[119.51669546055449,0.0,-0.0,132.71024672190032],"translation":[552.7263398065144,576.7207964230502]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[0.75,0.9330127018922193],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[0.2500000000000001,0.9330127018922194],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[0.0,0.5000000000000001],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[0.24999999999999978,0.06698729810778076],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[0.75,0.0669872981077807],"manipulator_type":"Anchor"},null,null]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5,6,7],"next_id":7},"style":{"stroke":{"color":{"red":0.39607844,"green":0.0,"blue":0.0,"alpha":1.0},"weight":10.0,"dash_lengths":[20.0,40.0],"dash_offset":1.0,"line_cap":"Round","line_join":"Round","line_join_miter_limit":4.0},"fill":{"Solid":{"red":0.21568628,"green":0.61960787,"blue":0.015686275,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[166.2099206517004,0.0,-0.0,171.3637166409004],"translation":[745.6846281131691,553.5287144716501]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[0.7057368997889272,0.12039914794770572,-0.12039914794770572,0.7057368997889272],"translation":[153.8440535943912,71.2188920841819]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":14549722863065794262,"auto_saved_document_identifier":0,"name":"Untitled Document","version":"0.0.15","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[3547353463246861192],{"selected":false,"expanded":false}],[[16075447627426792792],{"selected":false,"expanded":false}],[[],{"selected":false,"expanded":true}],[[3643066017826641933],{"selected":false,"expanded":false}],[[5260049129070821769],{"selected":false,"expanded":false}],[[17969178651777368768],{"selected":false,"expanded":false}],[[6240415495114589106],{"selected":false,"expanded":false}],[[6431150803620977111],{"selected":false,"expanded":false}],[[13779825621578452744],{"selected":false,"expanded":false}],[[10598788542653937530],{"selected":false,"expanded":false}]],"layer_range_selection_reference":[],"navigation_handler":{"pan":[-937.5907856355655,-408.64517115079275],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.1689738423032222,"tilting":false,"zoom":0.7159333254921297,"zooming":false,"snap_zoom":false,"mouse_position":[1198.0,253.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":10154885448817245701,"layer_ids":[4835915792681996579,10154885448817245700],"layers":[{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.0,0.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[0.0,1.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[1.0,1.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[1.0,0.0],"manipulator_type":"Anchor"},null,null]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5],"next_id":5},"style":{"stroke":null,"fill":{"Solid":{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[1920.0,0.0,-0.0,1080.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.0,0.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[0.0,1.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[1.0,1.0],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[1.0,0.0],"manipulator_type":"Anchor"},null,null]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5],"next_id":5},"style":{"stroke":null,"fill":{"Solid":{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[222.0,0.0,-0.0,213.0],"translation":[941.0,-398.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[0.7057368997889272,0.12039914794770572,-0.12039914794770572,0.7057368997889272],"translation":[153.8440535943912,71.2188920841819]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[4835915792681996579,10154885448817245700]},"properties_panel_message_handler":{"active_selection":null}} diff --git a/editor/src/consts.rs b/editor/src/consts.rs index a52bddb2..e828e918 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -74,7 +74,7 @@ pub const DEFAULT_FONT_FAMILY: &str = "Merriweather"; pub const DEFAULT_FONT_STYLE: &str = "Normal (400)"; // Document -pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.14"; // Remember to save a simple document and replace the test file `graphite-test-document.graphite` +pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.15"; // Remember to save a simple document and replace the test file `graphite-test-document.graphite` pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; pub const FILE_SAVE_SUFFIX: &str = ".graphite"; diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index 883a46b3..c7e7c056 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct FrontendDocumentDetails { + #[serde(rename = "isAutoSaved")] + pub is_auto_saved: bool, #[serde(rename = "isSaved")] pub is_saved: bool, pub name: String, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 20dd6262..7fbcd5b4 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -37,6 +37,7 @@ use serde::{Deserialize, Serialize}; pub struct DocumentMessageHandler { pub graphene_document: GrapheneDocument, pub saved_document_identifier: u64, + pub auto_saved_document_identifier: u64, pub name: String, pub version: String, @@ -68,6 +69,7 @@ impl Default for DocumentMessageHandler { Self { graphene_document: GrapheneDocument::default(), saved_document_identifier: 0, + auto_saved_document_identifier: 0, name: String::from("Untitled Document"), version: GRAPHITE_DOCUMENT_VERSION.to_string(), @@ -1354,10 +1356,22 @@ impl DocumentMessageHandler { .unwrap_or(0) } + pub fn is_auto_saved(&self) -> bool { + self.current_identifier() == self.auto_saved_document_identifier + } + pub fn is_saved(&self) -> bool { self.current_identifier() == self.saved_document_identifier } + pub fn set_auto_save_state(&mut self, is_saved: bool) { + if is_saved { + self.auto_saved_document_identifier = self.current_identifier(); + } else { + self.auto_saved_document_identifier = generate_uuid(); + } + } + pub fn set_save_state(&mut self, is_saved: bool) { if is_saved { self.saved_document_identifier = self.current_identifier(); diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 25ab867b..6bc5e77a 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -92,6 +92,7 @@ pub enum PortfolioMessage { OpenDocumentFileWithId { document_id: u64, document_name: String, + document_is_auto_saved: bool, document_is_saved: bool, document_serialized_content: String, }, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index cccaac31..3f19892f 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -49,6 +49,9 @@ impl MessageHandler { if let Some(document_id) = self.active_document_id { + if let Some(document) = self.active_document_mut() { + document.set_auto_save_state(true); + } responses.push_back(PortfolioMessage::AutoSaveDocument { document_id }.into()); } } @@ -58,6 +61,7 @@ impl MessageHandler { let document = DocumentMessageHandler::with_name_and_content(document_name, document_serialized_content); match document { Ok(mut document) => { + document.set_auto_save_state(document_is_auto_saved); document.set_save_state(document_is_saved); self.load_document(document, document_id, responses); } @@ -417,10 +415,10 @@ impl MessageHandler { if let Some(document) = self.active_document() { - if !document.is_saved() { - // Safe to unwrap since we know that there is an active document + if !document.is_auto_saved() { responses.push_back( PortfolioMessage::AutoSaveDocument { + // Safe to unwrap since we know that there is an active document document_id: self.active_document_id.unwrap(), } .into(), @@ -472,10 +470,11 @@ impl MessageHandler>(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c1c5727d..7b747760 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "license": "Apache-2.0", "dependencies": { "class-transformer": "^0.5.0", + "idb-keyval": "^6.2.0", "reflect-metadata": "^0.1.13", "vue": "^3.2.26" }, @@ -5632,6 +5633,14 @@ "postcss": "^8.1.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz", + "integrity": "sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==", + "dependencies": { + "safari-14-idb-fix": "^3.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8823,6 +8832,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safari-14-idb-fix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz", + "integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -15431,6 +15445,14 @@ "dev": true, "requires": {} }, + "idb-keyval": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz", + "integrity": "sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==", + "requires": { + "safari-14-idb-fix": "^3.0.0" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -17733,6 +17755,11 @@ "queue-microtask": "^1.2.2" } }, + "safari-14-idb-fix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz", + "integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==" + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 55f1dc54..520228ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "homepage": "https://graphite.rs", "dependencies": { "class-transformer": "^0.5.0", + "idb-keyval": "^6.2.0", "reflect-metadata": "^0.1.13", "vue": "^3.2.26" }, diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 49a2365e..6a9f6e98 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -303,7 +303,7 @@ export default defineComponent({ createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen), createLocalizationManager: createLocalizationManager(this.editor), createPanicManager: createPanicManager(this.editor, this.dialog), - createPersistenceManager: await createPersistenceManager(this.editor, this.portfolio), + createPersistenceManager: createPersistenceManager(this.editor, this.portfolio), }); // Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 94558964..840ece23 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -238,7 +238,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo function onBeforeUnload(e: BeforeUnloadEvent): void { const activeDocument = document.state.documents[document.state.activeDocumentIndex]; - if (!activeDocument.isSaved) editor.instance.triggerAutoSave(activeDocument.id); + if (activeDocument && !activeDocument.isAutoSaved) editor.instance.triggerAutoSave(activeDocument.id); // Skip the message if the editor crashed, since work is already lost if (editor.instance.hasCrashed()) return; diff --git a/frontend/src/io-managers/panic.ts b/frontend/src/io-managers/panic.ts index c4baba2f..2beece48 100644 --- a/frontend/src/io-managers/panic.ts +++ b/frontend/src/io-managers/panic.ts @@ -1,3 +1,4 @@ +import { wipeDocuments } from "@/io-managers/persistence"; import { type DialogState } from "@/state-providers/dialog"; import { type IconName } from "@/utility-functions/icons"; import { browserVersion, operatingSystem } from "@/utility-functions/platform"; @@ -43,7 +44,14 @@ function preparePanicDialog(header: string, details: string, panicDetails: strin callback: async () => window.open(githubUrl(panicDetails), "_blank"), props: { kind: "TextButton", label: "Report Bug", emphasized: false, minWidth: 96 }, }; - const jsCallbackBasedButtons = [reloadButton, copyErrorLogButton, reportOnGithubButton]; + const clearPersistedDataButton: TextButtonWidget = { + callback: async () => { + await wipeDocuments(); + window.location.reload(); + }, + props: { kind: "TextButton", label: "Clear Saved Data", emphasized: false, minWidth: 96 }, + }; + const jsCallbackBasedButtons = [reloadButton, copyErrorLogButton, reportOnGithubButton, clearPersistedDataButton]; return ["Warning", widgets, jsCallbackBasedButtons]; } diff --git a/frontend/src/io-managers/persistence.ts b/frontend/src/io-managers/persistence.ts index 136c6525..26e81ced 100644 --- a/frontend/src/io-managers/persistence.ts +++ b/frontend/src/io-managers/persistence.ts @@ -1,155 +1,100 @@ +import { createStore, del, get, set, update } from "idb-keyval"; + import { type PortfolioState } from "@/state-providers/portfolio"; -import { stripIndents } from "@/utility-functions/strip-indents"; import { type Editor } from "@/wasm-communication/editor"; import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument, TriggerSavePreferences, TriggerLoadAutoSaveDocuments, TriggerLoadPreferences } from "@/wasm-communication/messages"; -const GRAPHITE_INDEXED_DB_VERSION = 2; -const GRAPHITE_INDEXED_DB_NAME = "graphite-indexed-db"; +const graphiteStore = createStore("graphite", "store"); -const GRAPHITE_AUTO_SAVE_STORE = { name: "auto-save-documents", keyPath: "details.id" }; -const GRAPHITE_EDITOR_PREFERENCES_STORE = { name: "editor-preferences", keyPath: "key" }; +export function createPersistenceManager(editor: Editor, portfolio: PortfolioState): void { + // DOCUMENTS -const GRAPHITE_INDEXEDDB_STORES = [GRAPHITE_AUTO_SAVE_STORE, GRAPHITE_EDITOR_PREFERENCES_STORE]; + async function storeDocumentOrder(): Promise { + const documentOrder = portfolio.state.documents.map((doc) => String(doc.id)); -const GRAPHITE_AUTO_SAVE_ORDER_KEY = "auto-save-documents-order"; + await set("documents_tab_order", documentOrder, graphiteStore); + } -export function createPersistenceManager(editor: Editor, portfolio: PortfolioState): () => void { - async function initialize(): Promise { - // Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open - return new Promise((resolve) => { - const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION); + async function storeDocument(autoSaveDocument: TriggerIndexedDbWriteDocument): Promise { + await update>( + "documents", + (old) => { + const documents = old || {}; + documents[autoSaveDocument.details.id] = autoSaveDocument; + return documents; + }, + graphiteStore + ); - // Handle a version mismatch if `GRAPHITE_INDEXED_DB_VERSION` is now higher than what was saved in the database - dbOpenRequest.onupgradeneeded = (): void => { - const db = dbOpenRequest.result; + await storeDocumentOrder(); + } - // Wipe out all stores when a request is made to upgrade the database version to a newer one - GRAPHITE_INDEXEDDB_STORES.forEach((store) => { - if (db.objectStoreNames.contains(store.name)) db.deleteObjectStore(store.name); + async function removeDocument(id: string): Promise { + await update>( + "documents", + (old) => { + const documents = old || {}; + delete documents[id]; + return documents; + }, + graphiteStore + ); - db.createObjectStore(store.name, { keyPath: store.keyPath }); - }); - }; + await storeDocumentOrder(); + } - // Handle some other error by presenting it to the user - dbOpenRequest.onerror = (): void => { - const errorText = stripIndents` - Documents won't be saved across reloads and later visits. - This may be caused by Firefox's private browsing mode. - - Error on opening IndexDB: - ${dbOpenRequest.error} - `; - editor.instance.errorDialog("Document auto-save doesn't work in this browser", errorText); - }; + async function loadDocuments(): Promise { + const previouslySavedDocuments = await get>("documents", graphiteStore); + const documentOrder = await get("documents_tab_order", graphiteStore); + if (!previouslySavedDocuments || !documentOrder) return; - // Resolve the promise on a successful opening of the database connection - dbOpenRequest.onsuccess = (): void => { - resolve(dbOpenRequest.result); - }; + const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); + + const currentDocumentVersion = editor.instance.graphiteDocumentVersion(); + orderedSavedDocuments?.forEach(async (doc: TriggerIndexedDbWriteDocument) => { + if (doc.version !== currentDocumentVersion) { + await removeDocument(doc.details.id); + return; + } + + editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document); }); } - function storeDocumentOrder(): void { - // Make sure to store as string since JSON does not play nice with BigInt - const documentOrder = portfolio.state.documents.map((doc) => doc.id.toString()); - window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder)); + // PREFERENCES + + async function savePreferences(preferences: TriggerSavePreferences["preferences"]): Promise { + await set("preferences", preferences, graphiteStore); } - async function removeDocument(id: string, db: IDBDatabase): Promise { - const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readwrite"); - transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).delete(id); - storeDocumentOrder(); + async function loadPreferences(): Promise { + const preferences = await get>("preferences", graphiteStore); + if (!preferences) return; + + editor.instance.loadPreferences(JSON.stringify(preferences)); } - async function loadAutoSaveDocuments(db: IDBDatabase): Promise { - let promiseResolve: (value: void | PromiseLike) => void; - const promise = new Promise((resolve): void => { - promiseResolve = resolve; - }); - - // Open auto-save documents - const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readonly"); - const request = transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).getAll(); - - request.onsuccess = (): void => { - const previouslySavedDocuments: TriggerIndexedDbWriteDocument[] = 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 TriggerIndexedDbWriteDocument[]; - - const currentDocumentVersion = editor.instance.graphiteDocumentVersion(); - orderedSavedDocuments.forEach(async (doc: TriggerIndexedDbWriteDocument) => { - if (doc.version === currentDocumentVersion) { - editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document); - } else { - await removeDocument(doc.details.id, db); - } - }); - - promiseResolve(); - }; - - await promise; - } - - async function loadPreferences(db: IDBDatabase): Promise { - let promiseResolve: (value: void | PromiseLike) => void; - const promise = new Promise((resolve): void => { - promiseResolve = resolve; - }); - - // Open auto-save documents - const transaction = db.transaction(GRAPHITE_EDITOR_PREFERENCES_STORE.name, "readonly"); - const request = transaction.objectStore(GRAPHITE_EDITOR_PREFERENCES_STORE.name).getAll(); - - request.onsuccess = (): void => { - const preferenceEntries: { key: string; value: unknown }[] = request.result; - - const preferences: Record = {}; - preferenceEntries.forEach(({ key, value }) => { - preferences[key] = value; - }); - - editor.instance.loadPreferences(JSON.stringify(preferences)); - - promiseResolve(); - }; - - await promise; - } + // FRONTEND MESSAGE SUBSCRIPTIONS // Subscribe to process backend events - editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => { - const transaction = (await databaseConnection).transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readwrite"); - transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).put(autoSaveDocument); - - storeDocumentOrder(); - }); - editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => { - await removeDocument(removeAutoSaveDocument.documentId, await databaseConnection); - }); - editor.subscriptions.subscribeJsMessage(TriggerLoadAutoSaveDocuments, async () => { - await loadAutoSaveDocuments(await databaseConnection); - }); editor.subscriptions.subscribeJsMessage(TriggerSavePreferences, async (preferences) => { - Object.entries(preferences.preferences).forEach(async ([key, value]) => { - const storedObject = { key, value }; - - const transaction = (await databaseConnection).transaction(GRAPHITE_EDITOR_PREFERENCES_STORE.name, "readwrite"); - transaction.objectStore(GRAPHITE_EDITOR_PREFERENCES_STORE.name).put(storedObject); - }); + await savePreferences(preferences.preferences); }); editor.subscriptions.subscribeJsMessage(TriggerLoadPreferences, async () => { - await loadPreferences(await databaseConnection); + await loadPreferences(); + }); + editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => { + await storeDocument(autoSaveDocument); + }); + editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => { + await removeDocument(removeAutoSaveDocument.documentId); + }); + editor.subscriptions.subscribeJsMessage(TriggerLoadAutoSaveDocuments, async () => { + await loadDocuments(); }); - - const databaseConnection = initialize(); - - // Destructor - return () => { - databaseConnection.then((connection) => connection.close()); - }; +} + +export async function wipeDocuments(): Promise { + await del("documents_tab_order", graphiteStore); + await del("documents", graphiteStore); } diff --git a/frontend/src/utility-functions/rasterization.ts b/frontend/src/utility-functions/rasterization.ts index b776438d..40e7e3bd 100644 --- a/frontend/src/utility-functions/rasterization.ts +++ b/frontend/src/utility-functions/rasterization.ts @@ -2,13 +2,7 @@ import { replaceBlobURLsWithBase64 } from "@/utility-functions/files"; // Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type export async function rasterizeSVGCanvas(svg: string, width: number, height: number, backgroundColor?: string): Promise { - let promiseResolve: (value: HTMLCanvasElement | PromiseLike) => void | undefined; - const promise = new Promise((resolve) => { - promiseResolve = resolve; - }); - - // A canvas to render our svg to in order to get a raster image - // https://stackoverflow.com/questions/3975499/convert-svg-to-image-jpeg-png-etc-in-the-browser + // A canvas to render our SVG to in order to get a raster image const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; @@ -25,38 +19,35 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num const svgWithBase64Images = await replaceBlobURLsWithBase64(svg); // Create a blob URL for our SVG - const image = new Image(); const svgBlob = new Blob([svgWithBase64Images], { type: "image/svg+xml;charset=utf-8" }); const url = URL.createObjectURL(svgBlob); - image.onload = (): void => { - // Draw our SVG to the canvas - context?.drawImage(image, 0, 0, width, height); - // Clean up the SVG blob URL (once the URL is revoked, the SVG blob data itself is garbage collected after `svgBlob` goes out of scope) - URL.revokeObjectURL(url); - - promiseResolve(canvas); - }; + const image = new Image(); image.src = url; + await new Promise((resolve) => { + image.onload = (): void => resolve(); + }); - return promise; + // Draw our SVG to the canvas + context?.drawImage(image, 0, 0, width, height); + + // Clean up the SVG blob URL (once the URL is revoked, the SVG blob data itself is garbage collected after `svgBlob` goes out of scope) + URL.revokeObjectURL(url); + + return canvas; } export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise { - let promiseResolve: (value: Blob | PromiseLike) => void | undefined; - let promiseReject: () => void | undefined; - const promise = new Promise((resolve, reject) => { - promiseResolve = resolve; - promiseReject = reject; - }); + const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor); - rasterizeSVGCanvas(svg, width, height, backgroundColor).then((canvas) => { - // Convert the canvas to an image of the correct MIME type + // Convert the canvas to an image of the correct MIME type + const blob = await new Promise((resolve) => { canvas.toBlob((blob) => { - if (blob !== null) promiseResolve(blob); - else promiseReject(); + resolve(blob || undefined); }, mime); }); - return promise; + if (!blob) throw new Error("Converting canvas to blob data failed in rasterizeSVG()"); + + return blob; } diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index c1cffc1c..e203716c 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -37,6 +37,8 @@ export class UpdateOpenDocumentsList extends JsMessage { export abstract class DocumentDetails { readonly name!: string; + readonly isAutoSaved!: boolean; + readonly isSaved!: boolean; readonly id!: bigint | string; @@ -50,6 +52,11 @@ export class FrontendDocumentDetails extends DocumentDetails { readonly id!: bigint; } +export class IndexedDbDocumentDetails extends DocumentDetails { + @Transform(({ value }: { value: bigint }) => value.toString()) + id!: string; +} + export class TriggerIndexedDbWriteDocument extends JsMessage { document!: string; @@ -59,11 +66,6 @@ export class TriggerIndexedDbWriteDocument extends JsMessage { version!: string; } -export class IndexedDbDocumentDetails extends DocumentDetails { - @Transform(({ value }: { value: bigint }) => value.toString()) - id!: string; -} - export class TriggerIndexedDbRemoveDocument extends JsMessage { // Use a string since IndexedDB can not use BigInts for keys @Transform(({ value }: { value: bigint }) => value.toString()) diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 20a16b39..6755adee 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -207,6 +207,7 @@ impl JsEditorHandle { let message = PortfolioMessage::OpenDocumentFileWithId { document_id, document_name, + document_is_auto_saved: true, document_is_saved, document_serialized_content, }; diff --git a/frontend/wasm/src/helpers.rs b/frontend/wasm/src/helpers.rs index 52647534..e5c81066 100644 --- a/frontend/wasm/src/helpers.rs +++ b/frontend/wasm/src/helpers.rs @@ -9,7 +9,12 @@ use wasm_bindgen::prelude::*; /// When a panic occurs, notify the user and log the error to the JS console before the backend dies pub fn panic_hook(info: &panic::PanicInfo) { let header = "The editor crashed — sorry about that"; - let description = "An internal error occurred. Reload the editor to continue. Please report this by filing an issue on GitHub."; + let description = " + An internal error occurred. Please report this by filing an issue on GitHub.\n\ + \n\ + Reload the editor to continue. If this happens immediately on repeated reloads, clear saved data. + " + .trim(); error!("{}", info);