From e8857a540695d929b2aca01afa7f9b3d15a990b0 Mon Sep 17 00:00:00 2001 From: bat Date: Sun, 30 Apr 2023 03:25:03 +0000 Subject: [PATCH 1/9] give settings the path --- components/page-actions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/components/page-actions.js b/components/page-actions.js index e9a5c14..9eb8918 100644 --- a/components/page-actions.js +++ b/components/page-actions.js @@ -227,6 +227,7 @@ export class PageActions extends HTMLElement { 'm-settings-page-settings' ) settingsEl.cspProfiles = this.cspProfiles + settingsEl.path = this.path settingsEl.data = this.page.settings const h = document.createElement('h2') h.innerText = this.text.settings From 9c67f0c7418ee51bc1f4a6a47c0a484c385b4343 Mon Sep 17 00:00:00 2001 From: bat Date: Sun, 30 Apr 2023 03:28:50 +0000 Subject: [PATCH 2/9] add new settings components --- app.js | 12 ++++++++++++ sw.js | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 5b51a7e..fdab7b8 100644 --- a/app.js +++ b/app.js @@ -7,14 +7,26 @@ import { NavMenu } from "/components/nav-menu.js" import { Dialog } from "/dialog/dialog.js" import { ButtonGroup } from "/forms/button-group.js" import { Dropdown } from "/menu/dropdown.js" +import { NetworkSettings } from "/settings/network-settings.js" import { PageSettings } from "/settings/page-settings.js" +import { Connections } from "/settings/connections.js" +import { ConnectionEdit } from "/settings/connection-edit.js" customElements.define('m-layout', Layout) customElements.define('m-page', Page) customElements.define('m-page-actions', PageActions) +customElements.define( + 'm-settings-network-settings', NetworkSettings +) customElements.define( 'm-settings-page-settings', PageSettings ) +customElements.define( + 'm-settings-connections', Connections +) +customElements.define( + 'm-settings-connection-edit', ConnectionEdit +) customElements.define( 'm-file-group-page', FileGroupPage ) diff --git a/sw.js b/sw.js index 48a6727..58c5f18 100644 --- a/sw.js +++ b/sw.js @@ -19,7 +19,10 @@ async function initCache() { '/loader/editor-build.js', '/menu/dropdown.js', '/settings/page-settings.js', - ]) //10 + '/settings/network-settings.js', + '/settings/connections.js', + '/settings/connection-edit.js', + ]) } self.addEventListener("install", event => { From 03885398c52b294250508eb7fdc69e16c53cbca1 Mon Sep 17 00:00:00 2001 From: bat Date: Sun, 30 Apr 2023 09:12:25 +0000 Subject: [PATCH 3/9] apply inverse connections --- components/page-actions.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/components/page-actions.js b/components/page-actions.js index 9eb8918..a0fd001 100644 --- a/components/page-actions.js +++ b/components/page-actions.js @@ -237,7 +237,16 @@ export class PageActions extends HTMLElement { 'm-forms-button-group' ) bGroup.addPrimary(this.text.save, () => { - this.page.settings = settingsEl.data + const settingsData = settingsEl.data + this.page.settings = settingsData + for (const dir of ['outbound', 'inbound']) { + const otherDir = ( + dir === 'outbound' ? 'inbound' : 'outbound' + ) + this.applyInverseSettings( + settingsData, dir, otherDir + ) + } dialog.close() this.dispatchEvent(new CustomEvent( 'settings-change', {bubbles: true, composed: true} @@ -250,6 +259,32 @@ export class PageActions extends HTMLElement { dialog.open() } + applyInverseSettings(settingsData, dir, otherDir) { + const selfEntries = Object.entries( + settingsData.connections[dir] + ) + for (const [path, access] of selfEntries) { + const key = 'settings/page:' + path + let val = localStorage.getItem(key) + try { + if (val !== null) { + val = JSON.parse(val) + } + } catch (err) { + // ignore + } + const data = val ?? {} + data.connections = data.connections ?? {} + data.connections[otherDir] = ( + data.connections[otherDir] ?? {} + ) + data.connections[otherDir][this.path] = access + localStorage.setItem( + key, JSON.stringify(data) + ) + } + } + get language() { return this._language } From c0113e8c713f448b86066b4dcb3c19a91e21d969 Mon Sep 17 00:00:00 2001 From: bat Date: Sun, 30 Apr 2023 21:48:42 +0000 Subject: [PATCH 4/9] fix renaming settings & update inverse settings on rename --- components/page-actions.js | 62 +++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/components/page-actions.js b/components/page-actions.js index a0fd001..d811804 100644 --- a/components/page-actions.js +++ b/components/page-actions.js @@ -122,18 +122,36 @@ export class PageActions extends HTMLElement { } return } + const sKeyOld = 'settings/page:' + this.path + const sKeyNew = 'settings/page:' + newPath + const settingsJson = localStorage.getItem(sKeyOld) + if (settingsJson ?? true === true) { + localStorage.setItem(sKeyNew, settingsJson) + localStorage.removeItem(sKeyOld) + let settingsData + try { + settingsData = JSON.parse(settingsJson) + } catch (err) { + settingsData = {} + } + if (settingsData?.connections) { + for (const dir of ['outbound', 'inbound']) { + const otherDir = ( + dir === 'outbound' ? 'inbound' : 'outbound' + ) + this.applyInverseRename( + settingsData, dir, otherDir, this.path, newPath + ) + } + } + } else { + localStorage.removeItem(sKeyNew) + } localStorage.setItem( newPath, localStorage.getItem(this.path) ) - localStorage.setItem( - 'settings/page:' + newPath, - localStorage.getItem(this.path) - ) localStorage.removeItem(this.path) - localStorage.removeItem( - 'settings/page:' + newPath, - ) dialog.close() location.hash = newPath }) @@ -285,6 +303,36 @@ export class PageActions extends HTMLElement { } } + applyInverseRename( + settingsData, dir, otherDir, oldPath, newPath + ) { + const selfEntries = Object.entries( + settingsData.connections[dir] ?? {} + ) + for (const [path, access] of selfEntries) { + const key = 'settings/page:' + path + let val = localStorage.getItem(key) + try { + if (val !== null) { + val = JSON.parse(val) + } + } catch (err) { + // ignore + } + const data = val ?? {} + data.connections = data.connections ?? {} + data.connections[otherDir] = ( + data.connections[otherDir] ?? {} + ) + const accessValue = data.connections[otherDir][oldPath] + data.connections[otherDir][newPath] = accessValue + data.connections[otherDir][oldPath] = undefined + localStorage.setItem( + key, JSON.stringify(data) + ) + } + } + get language() { return this._language } From e7e5cdc500a59da286ee7d5a364da0ff519642ed Mon Sep 17 00:00:00 2001 From: bat Date: Mon, 1 May 2023 06:47:05 +0000 Subject: [PATCH 5/9] support put and get messages --- components/file-group-page.json | 363 ++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 components/file-group-page.json diff --git a/components/file-group-page.json b/components/file-group-page.json new file mode 100644 index 0000000..4895c5d --- /dev/null +++ b/components/file-group-page.json @@ -0,0 +1,363 @@ +const frameHtml = ` + + + Frame + + + + ${'<'}script type="module"> +let frame = undefined +addEventListener('message', event => { + const isChild = ( + frame !== undefined && event.source == frame.contentWindow + ) + if (isChild) { + const transferArg = ( + event.ports?.length ? [[...event.ports]] : [] + ) + parent.postMessage(event.data, '*', ...transferArg) + } else { + let isNew = false + const d = event.data + if (Array.isArray(d) && d[0] === 'srcdoc') { + isNew = frame === undefined + if (isNew) { + frame = document.createElement('iframe') + frame.sandbox = "allow-scripts allow-top-navigation" + } + frame.srcdoc = d[1] + if (isNew) { + document.body.appendChild(frame) + } + } else if (frame !== undefined) { + const transferArg = ( + event.ports?.length ? [[...event.ports]] : [] + ) + frame.contentWindow.postMessage( + event.data, '*', ...transferArg + ) + } + } +}) + ${' + +` + +export class FileGroupPage extends HTMLElement { + constructor() { + super() + this.attachShadow({mode: 'open'}) + this.viewLoaded = false + this.onceOnLoaded = undefined + this.isGroup = true + } + + connectedCallback() { + const style = document.createElement('style') + style.textContent = ` + :host { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + grid-template-areas: "main"; + flex-direction: column; + align-items: stretch; + } + iframe { + border: none; + margin: 0; + padding: 0; + grid-area: main; + width: 100%; + } + :host(.editing) iframe.view { + visibility: hidden; + } + :host(.viewing) iframe.edit { + visibility: hidden; + } + ` + this.shadowRoot.append(style) + this.initEditFrame() + this.initViewFrame() + this.editing = this.editing + addEventListener('message', this.handleMessage) + } + + disconnectedCallback() { + removeEventListener('message', this.handleMessage) + } + + initEditFrame() { + const frame = document.createElement('iframe') + frame.classList.add('edit') + const csp = this.csp + if (csp !== undefined) { + frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" + const url = new URL( + '/-/frame', location.href + ) + url.searchParams.set('csp', csp) + url.searchParams.set('html', frameHtml) + frame.src = url.href + } else { + frame.sandbox = "allow-scripts allow-top-navigation" + frame.srcdoc = frameHtml + } + frame.addEventListener('load', () => { + this.displayEdit() + }) + frame.addEventListener('message', message => { + this.handleEditMessage(message) + }) + this.editFrame = frame + this.shadowRoot.append(frame) + } + + initViewFrame() { + const frame = document.createElement('iframe') + frame.classList.add('view') + const csp = this.csp + if (csp !== undefined) { + frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" + const url = new URL( + '/-/frame', location.href + ) + url.searchParams.set('csp', csp) + url.searchParams.set('html', frameHtml) + frame.src = url.href + } else { + frame.sandbox = "allow-scripts allow-top-navigation" + frame.srcdoc = frameHtml + } + frame.addEventListener('load', () => { + this.viewLoaded = true + if (this.onceOnLoaded) { + this.onceOnLoaded() + this.onceOnLoaded = undefined + } + }) + this.viewFrame = frame + this.shadowRoot.append(frame) + } + + displayView(doc) { + const msg = ['srcdoc', doc] + if (this.viewLoaded) { + this.viewFrame.contentWindow.postMessage( + msg, '*' + ) + } else { + this.onceOnLoaded = () => { + this.viewFrame.contentWindow.postMessage( + msg, '*' + ) + } + } + } + + async displayEdit() { + const doc = await this.editorBuild.build() + const msg = ['srcdoc', doc] + this.editFrame.contentWindow.postMessage( + msg, '*' + ) + } + + handleMessage = event => { + const editWin = this.editFrame?.contentWindow + const viewWin = this.viewFrame?.contentWindow + if (editWin && event.source == editWin) { + this.handleEditMessage(event) + } else if (viewWin && event.source == viewWin) { + this.handleViewMessage(event) + } + } + + async handleViewMessage(event) { + if (Array.isArray(event.data)) { + if (['get', 'put'].includes(event.data[0])) { + await this.handleRequestMessage('view', event) + } + } + } + + async handleEditMessage(event) { + if (Array.isArray(event.data)) { + if (event.data[0] === 'ready') { + this.editFrame.contentWindow.postMessage( + ['doc', this.body], '*' + ) + } else if (event.data[0] === 'html') { + const html = event.data[1] + this.displayView(html) + } else if (event.data[0] === 'save') { + const doc = event.data[1] + this.body = doc + } else if (['get', 'put'].includes(event.data[0])) { + await this.handleRequestMessage('edit', event) + } + } + } + + async handleRequestMessage(frame, event) { + if (event.ports.length === 1) { + const port = event.ports[0] + const method = event.data[0] + const path = event.data[1] + const access = this.getAccess(path) + if (method === 'get') { + if (['read', 'readWrite'].includes(access)) { + port.postMessage({ + status: 200, + body: this.getPage(path), + }) + } else { + port.postMessage({ + status: 401, + }) + } + } else if (method === 'put') { + const body = event.data[2] + if (access === 'readWrite') { + this.setPage(path, body) + port.postMessage({ + status: 200, + }) + } else { + port.postMessage({ + status: 401, + }) + } + } + port.close() + } else { + throw new Error('request message without port') + } + } + + getAccess(path) { + const settings = this.settings + const outbound = settings.connections?.outbound + if (outbound && outbound[path]) { + return outbound[path] + } + } + + getPage(path) { + const body = localStorage.getItem(path) + try { + return JSON.parse(body) + } catch (err) { + return body + } + } + + setPage(path, body) { + let value = body + if (typeof value !== 'string') { + value = JSON.stringify(value) + } + localStorage.setItem(path, value) + } + + set body(value) { + try { + localStorage.setItem(this.path, value) + } catch (err) { + console.error(err) + } + } + + get body() { + try { + return localStorage.getItem(this.path) + } catch (err) { + console.error(err) + return '' + } + } + + set settings(value) { + try { + localStorage.setItem( + 'settings/page:' + this.path, + JSON.stringify(value) + ) + } catch (err) { + console.error(err) + } + } + + get settings() { + let data + try { + data = localStorage.getItem( + 'settings/page:' + this.path + ) + if (data === null || data === undefined) { + return {} + } + } catch (err) { + console.error(err) + return {} + } + try { + return JSON.parse(data) + } catch (err) { + return {} + } + } + + get csp() { + if (this.cspOff) { + return undefined + } else { + return ( + this.settings.networkAccess in this.cspProfiles ? + this.cspProfiles[this.settings.networkAccess] : + this.cspProfiles.local + ) + } + } + + set editing(value) { + this._editing = value + if (this.shadowRoot.host) { + const classes = this.shadowRoot.host.classList + if (this.editing) { + classes.add('editing') + classes.remove('viewing') + } else { + classes.add('viewing') + classes.remove('editing') + if (this.editFrame) { + this.editFrame.contentWindow.postMessage( + ['request-html'], '*' + ) + } + } + } + } + + get editing() { + return this._editing + } +} \ No newline at end of file From a37c2e3203fcd774fde6947d954ae11aab0edac1 Mon Sep 17 00:00:00 2001 From: bat Date: Mon, 1 May 2023 06:48:07 +0000 Subject: [PATCH 6/9] remove file w/ incorrect name --- components/file-group-page.json | 363 -------------------------------- 1 file changed, 363 deletions(-) delete mode 100644 components/file-group-page.json diff --git a/components/file-group-page.json b/components/file-group-page.json deleted file mode 100644 index 4895c5d..0000000 --- a/components/file-group-page.json +++ /dev/null @@ -1,363 +0,0 @@ -const frameHtml = ` - - - Frame - - - - ${'<'}script type="module"> -let frame = undefined -addEventListener('message', event => { - const isChild = ( - frame !== undefined && event.source == frame.contentWindow - ) - if (isChild) { - const transferArg = ( - event.ports?.length ? [[...event.ports]] : [] - ) - parent.postMessage(event.data, '*', ...transferArg) - } else { - let isNew = false - const d = event.data - if (Array.isArray(d) && d[0] === 'srcdoc') { - isNew = frame === undefined - if (isNew) { - frame = document.createElement('iframe') - frame.sandbox = "allow-scripts allow-top-navigation" - } - frame.srcdoc = d[1] - if (isNew) { - document.body.appendChild(frame) - } - } else if (frame !== undefined) { - const transferArg = ( - event.ports?.length ? [[...event.ports]] : [] - ) - frame.contentWindow.postMessage( - event.data, '*', ...transferArg - ) - } - } -}) - ${' - -` - -export class FileGroupPage extends HTMLElement { - constructor() { - super() - this.attachShadow({mode: 'open'}) - this.viewLoaded = false - this.onceOnLoaded = undefined - this.isGroup = true - } - - connectedCallback() { - const style = document.createElement('style') - style.textContent = ` - :host { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: 1fr; - grid-template-areas: "main"; - flex-direction: column; - align-items: stretch; - } - iframe { - border: none; - margin: 0; - padding: 0; - grid-area: main; - width: 100%; - } - :host(.editing) iframe.view { - visibility: hidden; - } - :host(.viewing) iframe.edit { - visibility: hidden; - } - ` - this.shadowRoot.append(style) - this.initEditFrame() - this.initViewFrame() - this.editing = this.editing - addEventListener('message', this.handleMessage) - } - - disconnectedCallback() { - removeEventListener('message', this.handleMessage) - } - - initEditFrame() { - const frame = document.createElement('iframe') - frame.classList.add('edit') - const csp = this.csp - if (csp !== undefined) { - frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" - const url = new URL( - '/-/frame', location.href - ) - url.searchParams.set('csp', csp) - url.searchParams.set('html', frameHtml) - frame.src = url.href - } else { - frame.sandbox = "allow-scripts allow-top-navigation" - frame.srcdoc = frameHtml - } - frame.addEventListener('load', () => { - this.displayEdit() - }) - frame.addEventListener('message', message => { - this.handleEditMessage(message) - }) - this.editFrame = frame - this.shadowRoot.append(frame) - } - - initViewFrame() { - const frame = document.createElement('iframe') - frame.classList.add('view') - const csp = this.csp - if (csp !== undefined) { - frame.sandbox = "allow-same-origin allow-scripts allow-top-navigation" - const url = new URL( - '/-/frame', location.href - ) - url.searchParams.set('csp', csp) - url.searchParams.set('html', frameHtml) - frame.src = url.href - } else { - frame.sandbox = "allow-scripts allow-top-navigation" - frame.srcdoc = frameHtml - } - frame.addEventListener('load', () => { - this.viewLoaded = true - if (this.onceOnLoaded) { - this.onceOnLoaded() - this.onceOnLoaded = undefined - } - }) - this.viewFrame = frame - this.shadowRoot.append(frame) - } - - displayView(doc) { - const msg = ['srcdoc', doc] - if (this.viewLoaded) { - this.viewFrame.contentWindow.postMessage( - msg, '*' - ) - } else { - this.onceOnLoaded = () => { - this.viewFrame.contentWindow.postMessage( - msg, '*' - ) - } - } - } - - async displayEdit() { - const doc = await this.editorBuild.build() - const msg = ['srcdoc', doc] - this.editFrame.contentWindow.postMessage( - msg, '*' - ) - } - - handleMessage = event => { - const editWin = this.editFrame?.contentWindow - const viewWin = this.viewFrame?.contentWindow - if (editWin && event.source == editWin) { - this.handleEditMessage(event) - } else if (viewWin && event.source == viewWin) { - this.handleViewMessage(event) - } - } - - async handleViewMessage(event) { - if (Array.isArray(event.data)) { - if (['get', 'put'].includes(event.data[0])) { - await this.handleRequestMessage('view', event) - } - } - } - - async handleEditMessage(event) { - if (Array.isArray(event.data)) { - if (event.data[0] === 'ready') { - this.editFrame.contentWindow.postMessage( - ['doc', this.body], '*' - ) - } else if (event.data[0] === 'html') { - const html = event.data[1] - this.displayView(html) - } else if (event.data[0] === 'save') { - const doc = event.data[1] - this.body = doc - } else if (['get', 'put'].includes(event.data[0])) { - await this.handleRequestMessage('edit', event) - } - } - } - - async handleRequestMessage(frame, event) { - if (event.ports.length === 1) { - const port = event.ports[0] - const method = event.data[0] - const path = event.data[1] - const access = this.getAccess(path) - if (method === 'get') { - if (['read', 'readWrite'].includes(access)) { - port.postMessage({ - status: 200, - body: this.getPage(path), - }) - } else { - port.postMessage({ - status: 401, - }) - } - } else if (method === 'put') { - const body = event.data[2] - if (access === 'readWrite') { - this.setPage(path, body) - port.postMessage({ - status: 200, - }) - } else { - port.postMessage({ - status: 401, - }) - } - } - port.close() - } else { - throw new Error('request message without port') - } - } - - getAccess(path) { - const settings = this.settings - const outbound = settings.connections?.outbound - if (outbound && outbound[path]) { - return outbound[path] - } - } - - getPage(path) { - const body = localStorage.getItem(path) - try { - return JSON.parse(body) - } catch (err) { - return body - } - } - - setPage(path, body) { - let value = body - if (typeof value !== 'string') { - value = JSON.stringify(value) - } - localStorage.setItem(path, value) - } - - set body(value) { - try { - localStorage.setItem(this.path, value) - } catch (err) { - console.error(err) - } - } - - get body() { - try { - return localStorage.getItem(this.path) - } catch (err) { - console.error(err) - return '' - } - } - - set settings(value) { - try { - localStorage.setItem( - 'settings/page:' + this.path, - JSON.stringify(value) - ) - } catch (err) { - console.error(err) - } - } - - get settings() { - let data - try { - data = localStorage.getItem( - 'settings/page:' + this.path - ) - if (data === null || data === undefined) { - return {} - } - } catch (err) { - console.error(err) - return {} - } - try { - return JSON.parse(data) - } catch (err) { - return {} - } - } - - get csp() { - if (this.cspOff) { - return undefined - } else { - return ( - this.settings.networkAccess in this.cspProfiles ? - this.cspProfiles[this.settings.networkAccess] : - this.cspProfiles.local - ) - } - } - - set editing(value) { - this._editing = value - if (this.shadowRoot.host) { - const classes = this.shadowRoot.host.classList - if (this.editing) { - classes.add('editing') - classes.remove('viewing') - } else { - classes.add('viewing') - classes.remove('editing') - if (this.editFrame) { - this.editFrame.contentWindow.postMessage( - ['request-html'], '*' - ) - } - } - } - } - - get editing() { - return this._editing - } -} \ No newline at end of file From efe43fd7e13af059ff94c44ebe4af3e11789b7aa Mon Sep 17 00:00:00 2001 From: bat Date: Mon, 1 May 2023 06:49:42 +0000 Subject: [PATCH 7/9] add support for get/put of connected pages --- components/file-group-page.js | 80 ++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/components/file-group-page.js b/components/file-group-page.js index 3125673..4895c5d 100644 --- a/components/file-group-page.js +++ b/components/file-group-page.js @@ -29,7 +29,10 @@ addEventListener('message', event => { frame !== undefined && event.source == frame.contentWindow ) if (isChild) { - parent.postMessage(event.data, '*') + const transferArg = ( + event.ports?.length ? [[...event.ports]] : [] + ) + parent.postMessage(event.data, '*', ...transferArg) } else { let isNew = false const d = event.data @@ -44,7 +47,12 @@ addEventListener('message', event => { document.body.appendChild(frame) } } else if (frame !== undefined) { - frame.contentWindow.postMessage(event.data, '*') + const transferArg = ( + event.ports?.length ? [[...event.ports]] : [] + ) + frame.contentWindow.postMessage( + event.data, '*', ...transferArg + ) } } }) @@ -184,6 +192,11 @@ export class FileGroupPage extends HTMLElement { } async handleViewMessage(event) { + if (Array.isArray(event.data)) { + if (['get', 'put'].includes(event.data[0])) { + await this.handleRequestMessage('view', event) + } + } } async handleEditMessage(event) { @@ -198,8 +211,71 @@ export class FileGroupPage extends HTMLElement { } else if (event.data[0] === 'save') { const doc = event.data[1] this.body = doc + } else if (['get', 'put'].includes(event.data[0])) { + await this.handleRequestMessage('edit', event) + } + } + } + + async handleRequestMessage(frame, event) { + if (event.ports.length === 1) { + const port = event.ports[0] + const method = event.data[0] + const path = event.data[1] + const access = this.getAccess(path) + if (method === 'get') { + if (['read', 'readWrite'].includes(access)) { + port.postMessage({ + status: 200, + body: this.getPage(path), + }) + } else { + port.postMessage({ + status: 401, + }) + } + } else if (method === 'put') { + const body = event.data[2] + if (access === 'readWrite') { + this.setPage(path, body) + port.postMessage({ + status: 200, + }) + } else { + port.postMessage({ + status: 401, + }) + } } + port.close() + } else { + throw new Error('request message without port') + } + } + + getAccess(path) { + const settings = this.settings + const outbound = settings.connections?.outbound + if (outbound && outbound[path]) { + return outbound[path] + } + } + + getPage(path) { + const body = localStorage.getItem(path) + try { + return JSON.parse(body) + } catch (err) { + return body + } + } + + setPage(path, body) { + let value = body + if (typeof value !== 'string') { + value = JSON.stringify(value) } + localStorage.setItem(path, value) } set body(value) {