this repo has no description
1import { invoke } from "@tauri-apps/api/core";
2import { Node } from "../structs/node";
3import { Tab } from "../structs/Tab";
4import { createSignal } from "solid-js";
5import { listen } from "@tauri-apps/api/event";
6import { getVersion } from "@tauri-apps/api/app";
7import { NodesByID } from "../Nodes/Nodes";
8import { save } from "@tauri-apps/plugin-dialog";
9import { ConfirmationManager } from "./ConfirmationManager";
10import { platform } from "@tauri-apps/plugin-os";
11
12export interface TabHashMap {
13 [details: string] : Tab;
14}
15
16export class NodeManager{
17 public static Instance: NodeManager;
18
19 private _selectedTab: string | null = null;
20 private _tabs: TabHashMap = {};
21
22 private _nodes: Node[] = [];
23
24 constructor(){
25 NodeManager.Instance = this;
26
27 setInterval(() => {
28 let tabs = Object.values(this._tabs).filter(x => x.needSync);
29 for(let tab of tabs){
30 invoke('sync_tab', {
31 graph: this._generateTabGraph(tab.id)[0],
32 id: tab.id,
33 name: tab.name,
34 location: tab.saveLocation,
35 saveState: tab.needsSave()
36 });
37
38 tab.needSync = false;
39 }
40 }, 1000);
41
42 listen('load_new_tab', ( ev: any ) => {
43 this._loadFromConfig(ev.payload.path, null, ev.payload.graph);
44 });
45
46 invoke('load_previous_tabs').then(async ( tabs: any ) => {
47 let version = await getVersion();
48
49 for(let tab of Object.entries<any>(tabs)){
50 let loaded_tab = await this._loadFromConfig(tab[1][2], tab[0], JSON.stringify({
51 tab_name: tab[1][1],
52 version,
53 graph: tab[1][0]
54 }));
55
56 if(loaded_tab)
57 loaded_tab.setNeedsSave(tab[1][3]);
58 };
59
60 this.UpdateConfig();
61 });
62
63 listen('prompt_to_close', async _ => {
64 let tabs = Object.values(this._tabs);
65 let tabsNeedingSaving = tabs.filter(x => x.needsSave());
66
67 for(let tab of tabsNeedingSaving){
68 await new Promise<void>(res => {
69 ConfirmationManager.Instance.ShowConfirmation(
70 `Discard Changes in ${tab.name}?`,
71 'If you close this tab without saving you will lose all changes.',
72 [
73 {
74 text: 'Save',
75 callback: async () => {
76 await this.SaveTab(tab);
77 res();
78 }
79 },
80 {
81 text: 'Don\'t Save',
82 callback: async () => {
83 res();
84 }
85 }
86 ]
87 )
88 });
89 }
90
91 invoke('close_app');
92 });
93 }
94
95
96 private _tabUpdateHook: ( tabs: TabHashMap ) => void = () => {};
97 private _tabChangeHook: () => void = () => {};
98
99 public CurrentTab(): Tab | null{
100 if(!this._selectedTab)return null
101 return this._tabs[this._selectedTab] || null;
102 }
103
104 public async AddTab( name: string, id: string | null = null ): Promise<Tab>{
105 let [ selected, setSelected ] = createSignal(false);
106 let [ needsSave, setNeedsSave ] = createSignal(false);
107
108 let tab: Tab = {
109 name: name,
110 id: id || await NodeManager.Instance.GetNewNodeId(),
111 nodes: [],
112 saveLocation: null,
113
114 selected,
115 setSelected,
116
117 needsSave,
118 setNeedsSave,
119
120 needSync: false
121 };
122
123 this._tabs[tab.id] = tab;
124
125 this.SelectTab(tab.id);
126 this._tabUpdateHook(this._tabs);
127
128 return tab;
129 }
130
131 public CloseTab( id: string ){
132 let tab = this._tabs[id];
133
134 let closeCB = () => {
135 if(this._selectedTab === id){
136 let tabs = Object.values(this._tabs);
137
138 if(tabs.length === 1){
139 this.SelectTab(null);
140 } else{
141 let index = tabs.indexOf(tab);
142 let nextTab = tabs[index + 1];
143
144 if(nextTab)
145 this.SelectTab(nextTab.id);
146 else
147 this.SelectTab(tabs[0].id);
148 }
149 }
150
151 invoke('discard_tab', { id: id });
152
153 delete this._tabs[id];
154 this._tabUpdateHook(this._tabs);
155 }
156
157 if(tab.needsSave()){
158 ConfirmationManager.Instance.ShowConfirmation(
159 'Discard Changes?',
160 'If you close this tab without saving you will lose all changes.',
161 [
162 {
163 text: 'Save',
164 callback: async () => {
165 await this.SaveTab(tab);
166 closeCB();
167 }
168 },
169 {
170 text: 'Don\'t Save',
171 callback: () => {
172 closeCB();
173 }
174 }
175 ]
176 )
177 } else{
178 closeCB();
179 }
180 }
181
182 public RenameTab( id: string, name: string ){
183 let tab = this._tabs[id];
184 if(!tab)return;
185
186 tab.name = name;
187 this._tabUpdateHook(this._tabs);
188 }
189
190 public SelectTab( id: string | null ){
191 if(this._selectedTab && this._tabs[this._selectedTab]){
192 let tab = this._tabs[this._selectedTab];
193
194 tab.setSelected(false);
195 tab.nodes = this._nodes;
196 }
197
198 this._selectedTab = id;
199 this._tabChangeHook();
200
201 if(this._selectedTab){
202 let tab = this._tabs[this._selectedTab];
203 if(!tab){
204 this._selectedTab = null;
205 return this._nodes = [];
206 }
207
208 tab.setSelected(true);
209 this._nodes = tab.nodes;
210 } else{
211 this._nodes = [];
212 }
213 }
214
215 public async SaveTab( tab: Tab, ignoreSaveLocation: boolean = false ){
216 let path =
217 tab.saveLocation && !ignoreSaveLocation ?
218 tab.saveLocation :
219 await save({ defaultPath: tab.name + '.macro', filters: [ { name: 'Macro Files', extensions: [ 'macro' ] } ] });
220
221 if(!path)throw new Error("Cannot save");
222
223 tab.saveLocation = path;
224 tab.setNeedsSave(false);
225
226 this._saveConfigToDisk(path, tab.id);
227 }
228
229 public HookTabUpdate( cb: ( tabs: TabHashMap ) => void ){
230 this._tabUpdateHook = cb;
231 }
232
233 public HookTabChange( cb: () => void ){
234 this._tabChangeHook = cb;
235 }
236
237
238 public AddNode( node: Node ){
239 if(!this._selectedTab)return;
240
241 this._nodes.push(node);
242 this.UpdateConfig();
243 }
244
245 public RemoveNode( node: Node ){
246 if(!this._selectedTab)return;
247
248 this._nodes = this._nodes.filter(x => x !== node);
249 this.UpdateConfig();
250 }
251
252 public GetNodes(): Node[] | null{
253 if(this._selectedTab)
254 return this._nodes;
255 else
256 return null;
257 }
258
259 public async GetNewNodeId(){
260 let encoder = new TextEncoder();
261 let data = encoder.encode(Date.now().toString() + Math.random().toString());
262 let hash = await window.crypto.subtle.digest("SHA-256", data); // Probably should get a better ID implementation
263
264 return btoa(String.fromCharCode(...new Uint8Array(hash)));
265 }
266
267
268 public UpdateConfig( needsSave = true ){
269 if(!this._selectedTab)return;
270 let tab = this._tabs[this._selectedTab];
271 if(!tab)return;
272
273 tab.nodes = this._nodes;
274 tab.needSync = true;
275
276 if(needsSave)tab.setNeedsSave(true);
277 }
278
279 private async _loadFromConfig( path: string | null, id: string | null, config: string ){
280 let json = JSON.parse(config);
281
282 if(
283 !json.tab_name ||
284 !json.version ||
285 !json.graph
286 )return;
287
288 let tab = await this.AddTab(json.tab_name, id);
289 tab.needSync = false;
290 tab.saveLocation = path;
291
292 this._nodes = [];
293
294 let graph = json.graph;
295
296 // Populate nodes
297 for (let i = 0; i < graph.length; i++) {
298 let node = graph[i];
299
300 let nod = new Node(node.pos, NodesByID[node.typeId], node.id);
301
302 nod.statics = node.statics;
303 await nod.onStaticsUpdate(nod);
304
305 this._nodes.push(nod);
306 }
307
308 // Populate node inputs
309 for (let i = 0; i < graph.length; i++) {
310 let configNode = graph[i];
311 let outputParentNode = this._nodes[i];
312
313 for (let j = 0; j < configNode.outputs.length; j++) {
314 let output = configNode.outputs[j];
315
316 for (let k = 0; k < output.connections.length; k++) {
317 let input = output.connections[k];
318 let node = this._nodes.find(x => x.id === input.node)!;
319
320 if(!node)continue;
321
322 let realInput = node.inputs.find(x => x.index === input.index);
323 let realOutput = outputParentNode.outputs[j];
324
325 if(realInput){
326 realInput.connections.push(realOutput);
327 realOutput.connections.push(realInput);
328 } else{
329 let realInput = {
330 name: input.name,
331 type: input.type,
332 parent: node,
333 connections: [ realOutput ],
334 index: input.index
335 };
336
337 node.inputs.push(realInput);
338 realOutput.connections.push(realInput);
339 }
340 }
341 }
342 }
343
344 tab.setNeedsSave(false);
345 tab.nodes = this._nodes;
346
347 tab.needSync = false;
348 if(!id)this.UpdateConfig(false);
349
350 return tab;
351 }
352
353 private _generateTabGraph( tabId: string | null ): [ any, Tab | null ]{
354 // Convert it into a structure we can actually save...
355
356 if(!tabId)return [ null, null ];
357 let tab = this._tabs[tabId];
358 if(!tab)return [ null, null ];
359
360 let nodesToSave = [];
361
362 for (let i = 0; i < tab.nodes.length; i++) {
363 let node = tab.nodes[i];
364
365 let nodeOutputs = [];
366 let nodeInputs = [];
367
368 for (let j = 0; j < node.outputs.length; j++) {
369 let output = node.outputs[j];
370
371 nodeOutputs.push({
372 name: output.name,
373 type: output.type,
374 connections: output.connections.map(x => { return {
375 name: x.name,
376 node: x.parent.id,
377 index: x.index,
378 type: x.type
379 }})
380 })
381 }
382
383 for (let j = 0; j < node.inputs.length; j++) {
384 let input = node.inputs[j];
385
386 nodeInputs.push({
387 name: input.name,
388 type: input.type,
389 connections: input.connections.map(x => { return {
390 name: x.name,
391 node: x.parent.id,
392 index: x.index,
393 type: x.type
394 }})
395 })
396 }
397
398 nodesToSave.push({
399 name: node.name,
400 id: node.id,
401 typeId: node.typeId,
402 pos: [ node.x, node.y ],
403 outputs: nodeOutputs,
404 inputs: nodeInputs,
405 statics: node.statics
406 })
407 }
408
409 return [ nodesToSave, tab ];
410 }
411
412 private async _saveConfigToDisk( path: string, tabId: string | null ){
413 let [ nodesToSave, tab ] = this._generateTabGraph(tabId);
414 if(!tab)return;
415
416 invoke('save_graph', { graph: JSON.stringify({
417 tab_name: tab.name,
418 version: await getVersion(),
419 platform: platform(),
420 graph: nodesToSave
421 }), path });
422 }
423}