From 3d78df31d60619f6207b9423e63b47e12fc5db7c Mon Sep 17 00:00:00 2001 From: octospacc Date: Fri, 30 May 2025 21:00:27 +0200 Subject: [PATCH] Allow exporting single notebooks and partial imports; Refactor main app structure; Add TypeScript config --- .gitignore | 5 +- build.sh | 21 ++ globals.d.ts | 6 + index.html | 153 -------- src/app/AppSettingsModal.js | 109 ++++++ src/app/Message.js | 65 ++++ src/app/NotebookSettingsModal.js | 105 ++++++ src/app/SystemData.js | 132 +++++++ app.js => src/app/app.js | 394 +++------------------ icon.png => src/app/icon.png | Bin manifest.json => src/app/manifest.json | 4 +- src/app/style.css | 100 ++++++ src/index.html | 52 +++ src/schema.json | 77 ++++ service-worker.js => src/service-worker.js | 6 +- tsconfig.json | 15 + 16 files changed, 743 insertions(+), 501 deletions(-) create mode 100644 build.sh create mode 100644 globals.d.ts delete mode 100644 index.html create mode 100644 src/app/AppSettingsModal.js create mode 100644 src/app/Message.js create mode 100644 src/app/NotebookSettingsModal.js create mode 100644 src/app/SystemData.js rename app.js => src/app/app.js (60%) rename icon.png => src/app/icon.png (100%) rename manifest.json => src/app/manifest.json (64%) create mode 100644 src/app/style.css create mode 100644 src/index.html create mode 100644 src/schema.json rename service-worker.js => src/service-worker.js (90%) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index fd6d1ed..06dc591 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ localforage.min.js marked.min.js -preact/ \ No newline at end of file +preact/ +/src/app/index.html +/src/app/schema.json.js +/src/app/service-worker.js \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..76d6c01 --- /dev/null +++ b/build.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -e + +cd ./src/app + +echo 'window.SCHEMA=' > ./schema.json.js +cat ../schema.json >> ./schema.json.js + +cp ../index.html ../service-worker.js ./ + +for lib in lib/*.js + do sed -i "s|||" ./index.html +done + +for mod in *.js + do sed -i "s|||" ./index.html +done + +for file in *.* lib/*.* lib/*/*.* + do sed -i "s|//files//|'/${file}', //files//|" ./service-worker.js +done diff --git a/globals.d.ts b/globals.d.ts new file mode 100644 index 0000000..728414f --- /dev/null +++ b/globals.d.ts @@ -0,0 +1,6 @@ +declare var + marked, localforage, htm, + h, render, createContext, + useState, useEffect, useCallback, useRef, useContext, + ModalHeader, SchemaForm, SchemaField, + SCHEMA, appMain: any; \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 6883275..0000000 --- a/index.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - WhichNot - - - - - - - - - - \ No newline at end of file diff --git a/src/app/AppSettingsModal.js b/src/app/AppSettingsModal.js new file mode 100644 index 0000000..710e82b --- /dev/null +++ b/src/app/AppSettingsModal.js @@ -0,0 +1,109 @@ +function AppSettingsModal({ctx}) { + const {html, AppContext, STRINGS} = ctx; + const {state, setState, getNotebook} = useContext(AppContext); + + const mapMessages = (messages, mapper) => Object.fromEntries(Object.entries(messages).map(([notebookId, messages]) => ([notebookId, mapper(messages)]))); + + const exportData = () => JSON.stringify({ + preferences: state.prefs, + notebooks: state.notebooks, + messages: mapMessages(state.encrypteds, messages => Object.values(messages)), + }, null, 2); + + const exportDataNotebook = (notebook) => JSON.stringify({ + notebooks: [notebook], + messages: {[notebook.id]: Object.values(state.encrypteds[notebook.id])}, + }, null, 2); + + const [exportTxt, setExportTxt] = useState(exportData()); + const [importTxt, setImportTxt] = useState(''); + const [importReset, setImportReset] = useState(false); + + const importData = () => { + try { + const obj = JSON.parse(importTxt); + // TODO: decrypt messages on import, otherwise the notebooks will appear empty until a reload + // TODO: delete duplicate notebooks when importing without reset + // TODO: block the app until import is complete + if (obj.notebooks && obj.messages) { + if (importReset) { + setState(s => ({ ...s, + prefs: obj.preferences || {}, + notebooks: obj.notebooks, + encrypteds: mapMessages(obj.messages, messages => Object.fromEntries(messages.map(message => [message.id, message]))), + })); + } else { + setState(s => ({ ...s, + prefs: { ...s.prefs, ...obj.preferences }, + notebooks: [ ...s.notebooks, ...obj.notebooks ], + encrypteds: { ...s.encrypteds, ...mapMessages(obj.messages, messages => Object.fromEntries(messages.map(message => [message.id, message]))) }, + })); + } + setState(s => ({ ...s, showAppSettings: false })); + } else { + alert(STRINGS.get('Invalid data format')); + } + } catch (err) { + console.error(err); + alert(STRINGS.get('Invalid JSON syntax')); + } + }; + + const AestheticsSettings = () => html` +

${STRINGS.get('Aesthetics')}

+ +

+ +

+ setState(s => ({ ...s, prefs: { ...s.prefs, messageInput: { ...s.prefs.messageInput, fontSize: (ev.target.value ? `${ev.target.value}pt` : '') } } }))} + onInput=${ev => ev.target.dispatchEvent(new InputEvent('change'))} /> +

+ +

+ `; + + return html` +
+ <${ModalHeader} title="App Settings"> + + + + <${AestheticsSettings} /> + +

${STRINGS.get('Export Data')}

+ + + +

${STRINGS.get('Import Data')}

+

-

- ${state.debugMode && html` -

Debug Experiments

-

- setForm(f => ({ ...f, id: ev.target.value }))} /> - -

-

- - -

-

- - -

-

- - -

- ${accesses.map(access => html` -

- ${access.id} - - -

- `)} - - `} -

- ${' '} - ${' '} -

-
- `; -} - function SearchModal() { const {state, setState, getNotebook} = useContext(AppContext); const {query, global} = state.searchModal; @@ -944,73 +704,25 @@ function SearchModal() { `; } -function AppSettingsModal() { - const {state, setState} = useContext(AppContext); - const [importTxt, setImportTxt] = useState(''); - const exportData = () => JSON.stringify({ - preferences: state.prefs, - notebooks: state.notebooks, - messages: Object.fromEntries(Object.entries(state.encrypteds).map(([key, values]) => ([key, Object.values(values)]))), - }, null, 2); - const doImport = () => { - try { - const obj = JSON.parse(importTxt); - if (obj.notebooks && obj.messages) { - setState(s => ({ ...s, - prefs: obj.preferences, - notebooks: obj.notebooks, - encrypteds: Object.fromEntries(Object.entries(obj.messages).map(([notebookId, messages]) => ([notebookId, Object.fromEntries(messages.map(message => [message.id, message]))]))), - })); - // window.location.reload(); - setState(s => ({ ...s, showAppSettings: false })); - } else { - alert(STRINGS.get('Invalid data format')); - } - } catch (err) { - console.error(err); - alert(STRINGS.get('Invalid JSON syntax')); - } - }; - return html` -
-
-

${STRINGS.get('App Settings')}

- -
-

${STRINGS.get('Aesthetics')}

-

-

- setState(s => ({ ...s, prefs: { ...s.prefs, messageInput: { ...s.prefs.messageInput, fontSize: (ev.target.value ? `${ev.target.value}pt` : '') } } }))} onInput=${ev => ev.target.dispatchEvent(new InputEvent('change'))} /> -

-

-

${STRINGS.get('Export Data')}

- -

${STRINGS.get('Import Data')}

-