storage.js (3.4 KB)
1 // Storage: localStorage persistence, file import/export, serialization 2 3 function scheduleSave() { 4 if (state.saveTimer) { 5 clearTimeout(state.saveTimer); 6 } 7 state.saveTimer = setTimeout(saveTasksToLocalStorage, 1000); 8 } 9 10 function saveTasksToLocalStorage() { 11 var serializedTasks = serializeTaskTree(state.taskPath[0]); 12 localStorage.setItem('taskTree', serializedTasks); 13 state.saveTimer = null; 14 } 15 16 function serializeTaskTree(task, depth = 0) { 17 var indentation = '\t'.repeat(depth); 18 var status = task.state === 0 ? '_' : (task.state === 1 ? 'x' : '?'); 19 var serialized = `${indentation}${status} ${task.text}\n`; 20 21 for (var subtask of task.subtasks) { 22 serialized += serializeTaskTree(subtask, depth + 1); 23 } 24 25 return serialized; 26 } 27 28 function deserializeTaskTree(serialized) { 29 var lines = serialized.split('\n').filter(line => line.trim() !== ''); 30 var root = { id: 'root', subtasks: [] }; 31 var stack = [{ task: root, depth: -1 }]; 32 33 for (var line of lines) { 34 var depth = (line.match(/^\t*/)[0] || '').length; 35 var status = line[depth]; 36 var text = line.slice(depth + 2); 37 38 var newTask = { 39 id: depth === 0 ? 'root' : generateId(), 40 text: text, 41 state: status === '_' ? 0 : (status === 'x' ? 1 : 2), 42 subtasks: [], 43 selectedSubtaskId: null 44 }; 45 46 while (stack.length > 1 && stack[stack.length - 1].depth >= depth) { 47 stack.pop(); 48 } 49 50 if (depth === 0) { 51 Object.assign(root, newTask); 52 } else { 53 stack[stack.length - 1].task.subtasks.push(newTask); 54 } 55 stack.push({ task: newTask, depth: depth }); 56 } 57 58 return root; 59 } 60 61 function loadTasksFromLocalStorage() { 62 var savedTasks = localStorage.getItem('taskTree'); 63 if (savedTasks) { 64 console.log('%cloaded tasks from local storage:', "color: green;"); 65 console.log(savedTasks); 66 return deserializeTaskTree(savedTasks); 67 } else { 68 return { id: 'root', text: 'todo', state: 0, subtasks: [{ id: generateId(), text: '', state: 0, subtasks: [] }], selectedSubtaskId: null }; 69 } 70 } 71 72 function handleSave(e) { 73 if ((e.metaKey || e.ctrlKey) && e.key === 's') { 74 e.preventDefault(); 75 saveTaskTreeToFile(); 76 } 77 } 78 79 function saveTaskTreeToFile() { 80 var serializedTasks = serializeTaskTree(state.taskPath[0]); 81 var rootTaskName = state.taskPath[0].text || 'Untitled'; 82 var date = new Date(); 83 var fileName = `${rootTaskName} - ${date.toLocaleString('default', { month: 'short' }).toLowerCase()} ${date.getDate()}, '${date.getFullYear().toString().slice(-2)}.txt`; 84 85 var blob = new Blob([serializedTasks], { type: 'text/plain' }); 86 var url = URL.createObjectURL(blob); 87 88 var a = document.createElement('a'); 89 a.href = url; 90 a.download = fileName; 91 a.click(); 92 93 URL.revokeObjectURL(url); 94 } 95 96 function handleOpen(e) { 97 if ((e.metaKey || e.ctrlKey) && e.key === 'o') { 98 e.preventDefault(); 99 openTaskTreeFromFile(); 100 } 101 } 102 103 function openTaskTreeFromFile() { 104 var input = document.createElement('input'); 105 input.type = 'file'; 106 input.accept = '.txt'; 107 input.onchange = function(event) { 108 var file = event.target.files[0]; 109 var reader = new FileReader(); 110 reader.onload = function(e) { 111 try { 112 var newRootTask = deserializeTaskTree(e.target.result); 113 if (confirm('Are you sure you want to overwrite the existing task tree?')) { 114 state.rootTask = newRootTask; 115 state.currentTask = state.rootTask; 116 state.taskPath = [state.currentTask]; 117 renderCurrentView(); 118 saveTasksToLocalStorage(); 119 } 120 } catch (error) { 121 alert(`Error importing task tree: ${error.message}`); 122 } 123 }; 124 reader.readAsText(file); 125 }; 126 input.click(); 127 }