diff --git a/main.go b/main.go index 1750ffc..7c4fa2c 100644 --- a/main.go +++ b/main.go @@ -25,9 +25,9 @@ import ( "time" ) -const VERSION = "v31" +const VERSION = "v34" const CONTEXT_USER = 1 -const SYNC_PAGINATION = 1 +const SYNC_PAGINATION = 200 var ( FlagGenerate bool diff --git a/static/js/app.mjs b/static/js/app.mjs index e44eabd..49157b4 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -63,7 +63,7 @@ export class App { classList.add('page-' + page) }) - _mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', ()=>{ + _mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', () => { this.preferences = this.getPreferences() }) @@ -93,9 +93,19 @@ export class App { // Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving. // Thus, the exception is acceptable to consequent use of alt+shift. const CTRL = !event.shiftKey && event.ctrlKey && !event.altKey + const SHIFT_CTRL = event.shiftKey && event.ctrlKey && !event.altKey const SHIFT_ALT = event.shiftKey && !event.ctrlKey && event.altKey const SHIFT_CTRL_ALT = event.shiftKey && event.ctrlKey && event.altKey + // Space is better handled with event.code + if (event.code.toUpperCase() == 'SPACE' && SHIFT_CTRL) { + event.preventDefault() + event.stopPropagation() + document.querySelector('n2-nodenavigator').open() + return + } + + switch (event.key.toUpperCase()) { case 'F2': this.nodeUI.renameNode() @@ -133,6 +143,11 @@ export class App { this.nodeUI.saveNode() break + case ' ': + if (!CTRL) { handled = false; break } + console.log('ctrl+space') + break + default: handled = false } @@ -221,7 +236,7 @@ export class App { const devPrefSet = localStorage.getItem('device_preference_set') || 'default' const userData = localStorage.getItem('user') || '{"default": {}}' const user = JSON.parse(userData) - return new N2PreferenceSet(devPrefSet, user.Preferences[devPrefSet]) + return new N2PreferenceSet(devPrefSet, user.Preferences[devPrefSet] || {}) }// }}} } @@ -403,8 +418,175 @@ class N2DragIcon extends CustomHTMLElement { }// }}} } +class N2NodeNavigator extends CustomHTMLElement { + static { + this.tmpl = document.createElement('template') + this.tmpl.innerHTML = ` + + + ` + } + constructor() {// {{{ + super(true) + this.rootNode = new Node({ UUID: ROOT_NODE, Name: 'Start', Special: true }, -1) + this.currNode = this.rootNode + + this.fieldSearch.addEventListener('input', () => { + this.render() + }) + + this.fieldSearch.addEventListener('keydown', event => { + if (event.key == 'Enter' && this.currNode !== null && this.lastSearchTermResultedInNode) { + globalThis._app.goToNode(this.currNode.UUID, false, false) + this.close() + } + }) + }// }}} + async open() {// {{{ + if (this.elDlg.open) + return + + this.currNode = null + this.fieldSearch.value = '' + + this.render() + this.elDlg.showModal() + }// }}} + close() {// {{{ + this.elDlg.close() + }// }}} + async render() {// {{{ + let currNode = this.rootNode + this.lastSearchTermResultedInNode = false + const searchPath = this.getSearchPath() + const path = [] + + searchLoop: + for (const search of searchPath) { + // string.split returns an empty string if last character is a space. + // We don't want to make a difference if that's the case. + if (search === '') + continue + + // fetched nodes hasn't fetched children automatically. + if (!currNode.hasFetchedChildren()) + await currNode.fetchChildren() + + // Normalized names of current node's children are matched against + // the current search term and compiled into a list. + // The algorithm stops when there are none or more than one. + const matches = [] + for (const childNode of currNode.Children) { + const childName = this.normalizeName(childNode.get('Name')) + if (childName.match(search)) + matches.push(childNode) + } + + switch (matches.length) { + case 0: + this.fieldSearch.classList.add('no-match') + this.lastSearchTermResultedInNode = false + break searchLoop + + case 1: + this.fieldSearch.classList.remove('no-match') + currNode = matches[0] + const separator = document.createElement('div') + separator.classList.add('separator') + separator.innerHTML = `>` + + const pathEl = document.createElement('div') + pathEl.innerText = currNode.get('Name') + path.push(separator, pathEl) + + this.lastSearchTermResultedInNode = true + continue + + default: + // More than one match. + this.lastSearchTermResultedInNode = false + this.fieldSearch.classList.remove('no-match') + break searchLoop + } + } + + // The currNode can be the last of the search and not having had it children fetched previously. + if (!currNode.hasFetchedChildren()) + await currNode.fetchChildren() + + this.elPath.replaceChildren(...path) + + // Children are rendered for the current node to be found by the search path. + const search = this.lastSearchTermResultedInNode ? '' : searchPath.slice(-1) + + const childNodes = [] + for (const childNode of currNode.Children) { + const childName = this.normalizeName(childNode.get('Name')) + if (!childName.match(search)) + continue + + const childEl = document.createElement('div') + childEl.classList.add('child-node') + childEl.innerText = `${this.normalizeName(childNode.get('Name'))}` + childNodes.push(childEl) + } + + this.currNode = currNode + globalThis.navNode = currNode.get('Name') + `, ${this.lastSearchTermResultedInNode}` + + this.elChildren.replaceChildren(...childNodes) + }// }}} + normalizeName(name) {// {{{ + return name.toLowerCase().replaceAll(/\s+/g, '_') + }// }}} + getSearchPath() {// {{{ + return this.fieldSearch.value.toLowerCase().split(/\s+/) + }// }}} +} + customElements.define('n2-crumbs', N2Crumbs) customElements.define('n2-crumb', N2Crumb) +customElements.define('n2-nodenavigator', N2NodeNavigator) customElements.define('n2-dragicon', N2DragIcon) // vim: foldmethod=marker diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs index d1fb7ae..3cb3de3 100644 --- a/static/js/lib/custom_html_element.mjs +++ b/static/js/lib/custom_html_element.mjs @@ -20,6 +20,7 @@ export class CustomHTMLElement extends HTMLElement { const fieldName = this.toElementName('field', field) this[fieldName] = el this._fields.set(this.toElementName('', field), el) + el.classList.add('field-' + el.dataset.field) } const name = el.dataset.el diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index 531626b..a086d85 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -328,6 +328,52 @@ export class NodeStore { this.nodes[ORPHANED_NODE] = new Node({ UUID: ORPHANED_NODE, Name: 'Orphaned nodes', Special: true }, -1) }// }}} + deleteAll() {// {{{ + this.db.close() + + const req = indexedDB.deleteDatabase('notes') + + req.onsuccess = async () => { + try { + console.log('Database deleted successfully') + + // Deleting the storage will log out the user. + await this.deleteStorage() + await this.deleteCaches() + await this.deleteServiceWorkers() + + alert('Database deleted, will now log out.') + location.href = '/login' + } catch (e) { + console.error(e) + alert(e.message) + } + } + + req.onerror = (event) => { + console.error('Error deleting database:', event.target.errorCode) + } + + req.onblocked = () => { + console.warn('Deletion blocked. Close all other tabs/connections to this DB.') + } + }// }}} + async deleteStorage() {// {{{ + // Clearing local- and session-storage in case + // something of value to evil people gets stored there. + localStorage.clear() + sessionStorage.clear() + console.log('Local- and session-storage cleared.') + }// }}} + async deleteCaches() {// {{{ + const keys = await caches.keys() + return Promise.all(keys.map(key => caches.delete(key))) + }// }}} + async deleteServiceWorkers() {// {{{ + const registrations = await navigator.serviceWorker.getRegistrations() + await Promise.all(registrations.map(r => r.unregister())) + }// }}} + node(uuid, dataIfUndefined, newLevel) {//{{{ let n = this.nodes[uuid] if (n === undefined && dataIfUndefined !== undefined) diff --git a/static/js/page_preferences.mjs b/static/js/page_preferences.mjs index dd85ebf..2c44d55 100644 --- a/static/js/page_preferences.mjs +++ b/static/js/page_preferences.mjs @@ -238,6 +238,7 @@ export class N2PreferenceSet extends CustomHTMLElement { super(true) this.name = name this.data = data + this.validateDefaultProfile() this.render() // Enable the save button when settings are modified. @@ -248,6 +249,16 @@ export class N2PreferenceSet extends CustomHTMLElement { this.elName.addEventListener('click', () => this.updateName()) this.elDelete.addEventListener('click', () => this.deleteSet()) }// }}} + validateDefaultProfile() {// {{{ + if (!this.data.hasOwnProperty('default')) + this.data.default = {} + + if (!this.data.default.hasOwnProperty('DownloadFiles')) + this.data.default.DownloadFiles = false + + if (!this.data.default.hasOwnProperty('DownloadImages')) + this.data.default.DownloadImages = false + }// }}} updateName() {// {{{ if (this.name == 'default') { alert('Can not change name of the default profile.') diff --git a/static/js/page_storage.mjs b/static/js/page_storage.mjs index f6f562f..ed3970b 100644 --- a/static/js/page_storage.mjs +++ b/static/js/page_storage.mjs @@ -2,7 +2,7 @@ import { CustomHTMLElement } from "./lib/custom_html_element.mjs" import { API } from 'api' export class N2PageStorage extends CustomHTMLElement { - static { + static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `