diff --git a/authentication/pkg.go b/authentication/pkg.go index c0b9a2e..9eb6245 100644 --- a/authentication/pkg.go +++ b/authentication/pkg.go @@ -8,9 +8,6 @@ import ( "github.com/jmoiron/sqlx" "github.com/lib/pq" - // Internal - appUser "notes2/user" - // Standard "database/sql" "encoding/hex" @@ -30,6 +27,12 @@ type Manager struct { ExpireDays int } +type User struct { + ID int + Username string + Name string +} + func httpError(w http.ResponseWriter, err error) { // {{{ j, _ := json.Marshal(struct { OK bool @@ -162,16 +165,16 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques mngr.log.Info("authentication", "username", request.Username, "status", "accepted") j, _ := json.Marshal(struct { OK bool - User appUser.User + User User Token string }{true, user, token}) w.Write(j) } // }}} -func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user appUser.User, err error) { // {{{ +func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user User, err error) { // {{{ var row *sql.Row row = mngr.db.QueryRow(` - SELECT id, username, name, preferences + SELECT id, username, name FROM public.user WHERE LOWER(username) = LOWER($1) AND @@ -180,21 +183,13 @@ func (mngr *Manager) Authenticate(username, password string) (authenticated bool username, password, ) - var data []byte - err = row.Scan(&user.ID, &user.Username, &user.Name, &data) + err = row.Scan(&user.ID, &user.Username, &user.Name) if err != nil && err.Error() == "sql: no rows in result set" { err = nil authenticated = false return } if err != nil { - authenticated = false - return - } - - err = json.Unmarshal(data, &user.Preferences) - if err != nil { - authenticated = false return } @@ -283,7 +278,7 @@ func (mngr *Manager) ChangePassword(username, currentPassword, newPassword strin changed = (rowsAffected == 1) return } // }}} -func (mngr *Manager) NewClientUUID(user appUser.User) (clientUUID string, err error) { // {{{ +func (mngr *Manager) NewClientUUID(user User) (clientUUID string, err error) { // {{{ // Each client session has its own UUID. // Loop through until a unique one is established. var proposedClientUUID string diff --git a/main.go b/main.go index 6e3cf94..e9bd88d 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( // Internal "notes2/authentication" "notes2/html_template" - appUser "notes2/user" "os" // Standard @@ -24,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v29" +const VERSION = "v27" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 @@ -135,8 +134,6 @@ func main() { // {{{ http.HandleFunc("/offline", pageOffline) http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) - http.HandleFunc("GET /user/preferences", authenticated(actionUserGetPreferences)) - http.HandleFunc("POST /user/preferences", authenticated(actionUserSetPreferences)) http.HandleFunc("/sync/from_server/count/{sequence}", authenticated(actionSyncFromServerCount)) http.HandleFunc("/sync/from_server/{sequence}/{offset}", authenticated(actionSyncFromServer)) @@ -181,7 +178,7 @@ func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.Respon } // User object is added to the context for the next handler. - user := appUser.NewUser(claims) + user := NewUser(claims) r = r.WithContext(context.WithValue(r.Context(), CONTEXT_USER, user)) Log.Debug("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username, "client", user.ClientUUID) @@ -269,7 +266,7 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{ // The purpose of the Client UUID is to avoid // sending nodes back once again to a client that // just created or modified it. - user := getUserSession(r) + user := getUser(r) changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) offset, _ := strconv.Atoi(r.PathValue("offset")) @@ -292,7 +289,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{ // The purpose of the Client UUID is to avoid // sending nodes back once again to a client that // just created or modified it. - user := getUserSession(r) + user := getUser(r) changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID) @@ -312,7 +309,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{ w.Write(j) } // }}} func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) + user := getUser(r) var err error uuid := r.PathValue("uuid") @@ -328,7 +325,7 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ }) } // }}} func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) + user := getUser(r) var err error uuid := r.PathValue("uuid") @@ -351,7 +348,7 @@ func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ }) } // }}} func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) + user := getUser(r) var err error uuid := r.PathValue("uuid") @@ -363,12 +360,12 @@ func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{ } responseData(w, map[string]any{ - "OK": true, - "Count": count, + "OK": true, + "Count": count, }) } // }}} func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) + user := getUser(r) body, _ := io.ReadAll(r.Body) var request struct { @@ -392,47 +389,6 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ }) } // }}} -func actionUserGetPreferences(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) - prefs, err := user.Preferences() - if err != nil { - httpError(w, err) - return - } - - responseData(w, map[string]any{ - "OK": true, - "Preferences": prefs, - }) -} // }}} -func actionUserSetPreferences(w http.ResponseWriter, r *http.Request) { // {{{ - session := getUserSession(r) - - // Verify the "default" profile is still there. - var newPrefs map[string]appUser.UserPreferences - body, _ := io.ReadAll(r.Body) - err := json.Unmarshal(body, &newPrefs) - if err != nil { - httpError(w, err) - return - } - - if _, found := newPrefs["default"]; !found { - httpError(w, fmt.Errorf("'default' profile missing.")) - return - } - - err = session.SetPreferences(newPrefs) - if err != nil { - httpError(w, err) - return - } - - responseData(w, map[string]any{ - "OK": true, - }) -} // }}} - func createNewUser(username string) { // {{{ reader := bufio.NewReader(os.Stdin) @@ -475,8 +431,7 @@ func changePassword(username string) { // {{{ fmt.Printf("\nPassword changed\n") } // }}} -func getUserSession(r *http.Request) appUser.UserSession { // {{{ - user, _ := r.Context().Value(CONTEXT_USER).(appUser.UserSession) - user.Db = db +func getUser(r *http.Request) UserSession { // {{{ + user, _ := r.Context().Value(CONTEXT_USER).(UserSession) return user } // }}} diff --git a/sql/00010.sql b/sql/00010.sql deleted file mode 100644 index ecd8ab4..0000000 --- a/sql/00010.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."user" ADD preferences jsonb DEFAULT '{}' NOT NULL; diff --git a/static/css/notes2.css b/static/css/notes2.css index 7fdea0b..a48a319 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -73,10 +73,9 @@ button { 1fr; } - /* The other pages just gets the whole page without dividing it up. */ - &:not(.page-node) { + &.page-history { grid-template-areas: - "tree-expander tree pad1 n2-page pad2" + "tree-expander tree pad1 n2-pagehistory pad2" ; grid-template-columns: @@ -246,6 +245,7 @@ button { #notes2 { &.page-node { + #page-root { display: none; } @@ -260,7 +260,7 @@ button { display: contents; n2-pagestorage { - grid-area: n2-page; + grid-area: content; } } } @@ -268,14 +268,9 @@ button { &.page-history { #page-history { display: grid; - grid-area: n2-page; - } - } + grid-area: n2-pagehistory; - &.page-preferences { - #page-preferences { - display: block; - grid-area: n2-page; + n2-pagehistory {} } } @@ -287,6 +282,7 @@ button { #page-root { display: contents !important; } + } } diff --git a/static/images/icon_menu.svg b/static/images/icon_menu.svg index cfdd1e8..e60bdee 100644 --- a/static/images/icon_menu.svg +++ b/static/images/icon_menu.svg @@ -2,18 +2,18 @@ + + + menuhamburger + transform="translate(-146.57917,-92.339583)"> + menu + hamburger + + + diff --git a/static/js/api.mjs b/static/js/api.mjs index 26a19de..3fff10a 100644 --- a/static/js/api.mjs +++ b/static/js/api.mjs @@ -1,7 +1,7 @@ export class API { // query resolves into the JSON data produced by the application, or an exception with 'type' and 'error' properties. static async query(method, path, request) { - try { + return new Promise((resolve, reject) => { const body = JSON.stringify(request) const headers = {} @@ -12,22 +12,33 @@ export class API { headers.Authorization = `Bearer ${token}` } - const res = await fetch(path, { method, headers, body }) - // An HTTP communication level error occured. - if (!res.ok || res.status != 200) - throw new Error('HTTP error', { cause: { type: 'http', error: res, }}) - - // Application level response are handled here. - const json = await res.json() - if (!json.OK) - throw new Error(json.Error, { cause: { type: 'application', application: json, }}) - - return json - - } catch (err) { - // Catch any other errors from fetch. - throw new Error(err.message, { cause: { type: 'http', error: err, }}) - } + fetch(path, { method, headers, body }) + .then(response => { + // An HTTP communication level error occured. + if (!response.ok || response.status != 200) + return reject({ + type: 'http', + error: response, + }) + return response.json() + }) + .then(json => { + // Application level response are handled here. + if (!json.OK) + return reject({ + type: 'application', + error: json.Error, + application: json, + }) + resolve(json) + }) + .catch(err => + // Catch any other errors from fetch. + reject({ + type: 'http', + error: err, + })) + }) } static hasAuthenticationToken() {//{{{ diff --git a/static/js/app.mjs b/static/js/app.mjs index 90bad39..c556341 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -2,7 +2,6 @@ import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { N2Sidebar } from 'sidebar' import { Node } from 'node' -import { N2PreferenceSet } from './page_preferences.mjs' export class App { static PAGES = ['node', 'history', 'storage'] @@ -15,8 +14,6 @@ export class App { this.nodeUI = document.getElementById('note') this.dragIcon = new N2DragIcon() - this.preferences = this.getPreferences() - this.sidebar.render().then(sidebar => { document.getElementById('tree').append(sidebar) document.getElementById('tree-nodes')?.focus() @@ -64,11 +61,6 @@ export class App { classList.add('page-' + page) }) - _mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', ()=>{ - this.preferences = this.getPreferences() - console.log(this.preferences.data) - }) - window.addEventListener('keydown', event => this.keyHandler(event)) window.addEventListener('popstate', event => this.popState(event)) document.getElementById('notes2').addEventListener('click', event => { @@ -219,12 +211,6 @@ export class App { let classList = document.querySelector('#main-page').classList return classList.contains(page) }// }}} - getPreferences() {// {{{ - 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]) - }// }}} } class N2Crumbs extends CustomHTMLElement { diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs index d1fb7ae..2cec808 100644 --- a/static/js/lib/custom_html_element.mjs +++ b/static/js/lib/custom_html_element.mjs @@ -1,17 +1,7 @@ -/* Use data-el or data-field attribute. - * Element with data-el="hum-ding" is accessible as this.elHumDing and fields with - * data-field="long-dong" as this.fieldLongDong. - * - * All field values can be retrieved with fieldValues() and uses the data-field attribute - * as LongDong as key. - */ - export class CustomHTMLElement extends HTMLElement { constructor(useShadow) {// {{{ super() - this._fields = new Map() - const workOn = useShadow ? this.attachShadow({ mode: 'open' }) : this workOn.appendChild(this.constructor.tmpl.content.cloneNode(true)) workOn.querySelectorAll('*').forEach(el => { @@ -19,7 +9,6 @@ export class CustomHTMLElement extends HTMLElement { if (field !== undefined) { const fieldName = this.toElementName('field', field) this[fieldName] = el - this._fields.set(this.toElementName('', field), el) } const name = el.dataset.el @@ -30,22 +19,39 @@ export class CustomHTMLElement extends HTMLElement { } }) }// }}} - allFields() {// {{{ - return this._fields - }// }}} - fieldValues() {// {{{ - const state = {} - for (const [name, field] of this._fields) { - if (field.tagName.toLowerCase() == 'input' && field.getAttribute('type').toLowerCase() == 'checkbox') - state[name] = field.checked - else - state[name] = field.value - - } - return state - }// }}} toElementName(prefix, str) {// {{{ str = prefix + '-' + str return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', '')) }// }}} } + +export class StupidPreactCustomHTMLElement extends HTMLElement { + constructor() {// {{{ + super() + + // Stupid stuff because of Preact. + this.clonedNodes = this.constructor.tmpl.content.cloneNode(true) + this.clonedNodes.querySelectorAll('*').forEach(el => { + const field = el.dataset.field + if (field !== undefined) { + const fieldName = this.toElementName('field', field) + this[fieldName] = el + } + + const name = el.dataset.el + if (name !== undefined) { + const elName = this.toElementName('el', name) + this[elName] = el + el.classList.add('el-' + name) + } + }) + }// }}} + toElementName(prefix, str) {// {{{ + str = prefix + '-' + str + return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', '')) + }// }}} + connectedCallback() {// {{{ + // Stupid stuff because of Preact. + this.appendChild(this.clonedNodes) + }// }}} +} diff --git a/static/js/marked_position.mjs b/static/js/marked_position.mjs index 8c81eb4..63f76f7 100644 --- a/static/js/marked_position.mjs +++ b/static/js/marked_position.mjs @@ -94,7 +94,7 @@ export class MarkedPosition { constructor() {// {{{ window.marked_setpos = (event) => this.setpos(event) window.marked_changecheckbox = (event) => this.changecheckbox(event) - window.marked_copy_to_clipboard = (event, tagname) => this.copy_to_clipboard(event, tagname) + window.marked_copy_to_clipboard = (event) => this.copy_to_clipboard(event) this.render() }// }}} setpos(event) {// {{{ @@ -120,7 +120,7 @@ export class MarkedPosition { } }) }// }}} - async copy_to_clipboard(event, tagname) {// {{{ + async copy_to_clipboard(event) {// {{{ if (!event.shiftKey) return @@ -134,11 +134,8 @@ export class MarkedPosition { const text = event.target.innerText await navigator.clipboard.writeText(text) - - const tagClasslist = event.target.closest(tagname).classList - tagClasslist.add('copy') - setTimeout(()=>tagClasslist.remove('copy'), 250) - + event.target.classList.add('copy') + setTimeout(()=>event.target.classList.remove('copy'), 250) } catch (err) { console.error('Failed to copy: ', err) alert('Failed to copy: ', err) @@ -146,6 +143,7 @@ export class MarkedPosition { }// }}} render() {// {{{ + const markedObject = this this.marked = new Marked() this.marked.use(markedTokenPosition()) this.marked.use({ @@ -190,12 +188,12 @@ export class MarkedPosition { const code = token.text.replace(other.endingNewline, '') + '\n' if (!langString) { - return `
`
+						return `
`
 							+ (token.escaped ? code : escapeHtmlEntities(code, true))
 							+ '
\n' } - return `
'
 						+ (token.escaped ? code : escapeHtmlEntities(code, true))
@@ -285,7 +283,7 @@ export class MarkedPosition {
 				},
 
 				codespan(token) {
-					return `${escapeHtmlEntities(token.text, true)}`
+					return `${escapeHtmlEntities(token.text, true)}`
 				},
 
 				br(token) {
diff --git a/static/js/page_preferences.mjs b/static/js/page_preferences.mjs
deleted file mode 100644
index 9655278..0000000
--- a/static/js/page_preferences.mjs
+++ /dev/null
@@ -1,283 +0,0 @@
-import { CustomHTMLElement } from "./lib/custom_html_element.mjs"
-import { API } from './api.mjs'
-
-export class N2PagePreferences extends CustomHTMLElement {
-	static {// {{{
-		this.tmpl = document.createElement('template')
-		this.tmpl.innerHTML = `
-			
-			

Preferences

- -
Changes preferences to not download images or files on the device doesn't remove the already downloaded data.
- -
-
Device preference set
- -
- -
- - - - ` - }// }}} - constructor() {// {{{ - super(true) - this.sets = [] - - this.elNewSet.addEventListener('click', () => this.newSet()) - this.elSave.addEventListener('click', () => this.save()) - this.elDevPreferenceSet.addEventListener('change', event=>this.changePreferenceSet(event)) - - window._mbus.subscribe('SHOW_PAGE', async event => { - if (event.detail.data?.page == 'preferences') { - this.sets = await this.getPreferenceSets() - this.render() - } - }) - - window._mbus.subscribe('PREFERENCE_SET_MODIFIED', () => this.preferencesModified()) - window._mbus.subscribe('PREFERENCE_SET_DELETE', event => this.preferencesDelete(event.detail.data.set)) - }// }}} - sortSets(a, b) {// {{{ - if (a.name == 'default') return -1 - if (b.name == 'default') return 1 - - if (a.name.toLowerCase() < b.name.toLowerCase()) return -1 - if (a.name.toLowerCase() > b.name.toLowerCase()) return 1 - - return 0 - }// }}} - async render() {// {{{ - try { - this.sets.sort(this.sortSets) - this.elSets.replaceChildren(...this.sets) - - const setNames = this.sets.entries().map(([i, set]) => { - const optn = document.createElement('option') - optn.innerText = set.name - return optn - }) - this.elDevPreferenceSet.replaceChildren(...setNames) - } catch (e) { - console.error(e) - alert(e.message) - } - }// }}} - async getPreferenceSets() {// {{{ - const userData = localStorage.getItem('user') - if (userData === null) - throw new Error('Could not find user in localStorage') - - const user = JSON.parse(userData) - const prefsData = user.Preferences - - if (prefsData === undefined) - throw new Error('User object is missing preferences') - - if (!prefsData.hasOwnProperty('default')) - throw new Error('The "default" preferences set is missing') - - return Object.keys(prefsData).map(name => new N2PreferenceSet(name, prefsData[name])) - }// }}} - async retrieveServerPreferences() {// {{{ - try { - API.query('GET', '/user/preferences') - } catch (e) { - console.error(e) - alert(`Error retrieving preferences: ${e.message}`) - } - }// }}} - changePreferenceSet(event) {// {{{ - this.preferencesModified() - }// }}} - newSet() {// {{{ - let name = prompt("Name for new preference set") - if (!name) - return - - name = name.trim() - if (name === '') - return - - if (name == 'default') { - alert(`Name can't be "default".`) - return - } - - const exists = this.sets.some(s => s.name.toLowerCase() == name.toLowerCase()) - if (exists) { - alert(`Set with name "${name}" already exist.`) - return - } - - this.sets.push(new N2PreferenceSet(name, {})) - this.preferencesModified() - this.render() - }// }}} - preferencesModified() {// {{{ - this.elSave.removeAttribute('disabled') - }// }}} - preferencesDelete(deleteSet) {// {{{ - if (deleteSet.name == 'default') { - alert("Can't delete the default set.") - return - } - - if (!confirm(`Confirm deleting "${deleteSet.name}"`)) - return - - this.sets = this.sets.filter(set => { - return !(set.name === deleteSet.name) - }) - - this.preferencesModified() - this.render() - }// }}} - async save() {// {{{ - try { - let newPrefs = {} - this.sets.forEach(s => { - const setState = s.getState() - newPrefs[setState.name] = setState.state - }) - - // Throws exception on both HTTP and application errors. - await API.query('POST', '/user/preferences', newPrefs) - - const userData = localStorage.getItem('user') - const user = JSON.parse(userData) - user.Preferences = newPrefs - localStorage.setItem('user', JSON.stringify(user)) - localStorage.setItem('device_preference_set', this.elDevPreferenceSet.value) - _mbus.dispatch('DEVICE_PREFERENCE_SET_UPDATED') - } catch (e) { - console.error(e) - alert(e.message) - } finally { - this.elSave.setAttribute('disabled', true) - } - - }// }}} -} -customElements.define('n2-pagepreferences', N2PagePreferences) - -// Preferences is a set of preferences, of which there can be many named. -export class N2PreferenceSet extends CustomHTMLElement { - static {// {{{ - this.tmpl = document.createElement('template') - this.tmpl.innerHTML = ` - - -
-
-
-
- -
- - -
- - ` - }// }}} - constructor(name, data) {// {{{ - super(true) - this.name = name - this.data = data - this.render() - - // Enable the save button when settings are modified. - this.allFields().forEach(f => - f.addEventListener('input', () => _mbus.dispatch('PREFERENCE_SET_MODIFIED')) - ) - - this.elName.addEventListener('click', () => this.updateName()) - this.elDelete.addEventListener('click', () => this.deleteSet()) - }// }}} - updateName() {// {{{ - if (this.name == 'default') { - alert('Can not change name of the default profile.') - return - } - - const name = prompt("Change name", this.name) - if (!name) - return - - this.name = name - this.render() - _mbus.dispatch('PREFERENCE_SET_MODIFIED') - }// }}} - deleteSet() {// {{{ - _mbus.dispatch('PREFERENCE_SET_DELETE', { set: this }) - }// }}} - render() {// {{{ - this.elName.innerText = this.name - - this.fieldDownloadImages.checked = this.data.DownloadImages - this.fieldDownloadFiles.checked = this.data.DownloadFiles - }// }}} - getState() {// {{{ - const name = this.name.trim() - if (name === '') - throw new Error('Name can not be empty.') - - return { - name: this.name.trim(), - state: this.fieldValues(), - } - }// }}} -} -customElements.define('n2-preferenceset', N2PreferenceSet) diff --git a/static/js/page_storage.mjs b/static/js/page_storage.mjs index a007130..931a718 100644 --- a/static/js/page_storage.mjs +++ b/static/js/page_storage.mjs @@ -13,10 +13,7 @@ export class N2PageStorage extends CustomHTMLElement { constructor() { super() - window._mbus.subscribe('SHOW_PAGE', event => { - if (event.detail.data?.page == 'storage') - this.render() - }) + window._mbus.subscribe('SHOW_PAGE', () => this.render()) } async render() { const countNodes = await globalThis.nodeStore.nodeCount() diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs index 6cd5814..129b9d1 100644 --- a/static/js/sidebar.mjs +++ b/static/js/sidebar.mjs @@ -128,7 +128,6 @@ export class N2Sidebar extends CustomHTMLElement { this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search')) this.elSync.addEventListener('click', () => _sync.run()) this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false)) - this.elSettings.addEventListener('click', ()=> _mbus.dispatch('SHOW_PAGE', { page: 'preferences' })) this.elHideTree.addEventListener('click', event => { event.stopPropagation() _mbus.dispatch('TREE_EXPANSION', { expand: false }) @@ -198,8 +197,9 @@ export class N2Sidebar extends CustomHTMLElement { this.expandedNodes[UUID] = false return this.expandedNodes[UUID] }//}}} - async setNodeExpanded(node, value) {//{{{ + setNodeExpanded(node, value) {//{{{ let expanded = this.expandedNodes[node.UUID] + if (expanded === undefined) { this.expandedNodes[node.UUID] = false expanded = false @@ -249,6 +249,8 @@ export class N2Sidebar extends CustomHTMLElement { // Holding shift down does it recursively. case Space: case 'Enter': + if (n.UUID === ROOT_NODE) + return const expanded = this.getNodeExpanded(n.UUID) if (event.shiftKey) { this.recursiveExpand(n, !expanded) @@ -257,31 +259,38 @@ export class N2Sidebar extends CustomHTMLElement { } break + case 'g': case 'Home': this.navigateTop() break + case 'G': case 'End': this.navigateBottom() break + case 'j': case 'ArrowDown': await this.navigateDown(this.selectedNode) break + case 'k': case 'ArrowUp': await this.navigateUp(this.selectedNode) break + case 'h': case 'ArrowLeft': await this.navigateLeft(this.selectedNode) break + case 'l': case 'ArrowRight': await this.navigateRight(this.selectedNode) break default: + // nonsole.log(event.key) handled = false } @@ -403,26 +412,23 @@ export class N2Sidebar extends CustomHTMLElement { }//}}} async navigateTop() {//{{{ const root = await nodeStore.get(ROOT_NODE) - _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.UUID, dontPush: false, dontExpand: true }) + if (root.Children.length === 0) + return + _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: false, dontExpand: true }) }//}}} async navigateBottom() {//{{{ - const orphaned = await nodeStore.get(ORPHANED_NODE) - - if (!orphaned.hasChildren() || this.getNodeExpanded(orphaned.UUID)) { - _mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.UUID, dontPush: false, dontExpand: true }) + const root = await nodeStore.get(ROOT_NODE) + if (root.Children.length === 0) return - } - /* TODO - fix this when orphaned nodes are implemented. - const toplevel = orphaned.Children[orphaned.Children.length - 1] + const toplevel = root.Children[root.Children.length - 1] const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID) if (toplevelExpanded) { const lastnode = this.getLastExpandedNode(toplevel) _mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: false, dontExpand: true }) } else - _mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.Children[orphaned.Children.length - 1]?.UUID, dontPush: false, dontExpand: true }) - */ + _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: false, dontExpand: true }) }//}}} getParentWithNextSibling(node) {//{{{ @@ -443,10 +449,6 @@ export class N2Sidebar extends CustomHTMLElement { if (state) await this.setNodeExpanded(node, true) - // An expanded node needs to have its children fetched. - if (!node.hasFetchedChildren()) - await node.fetchChildren() - for (const child of node.Children) await this.recursiveExpand(child, state) @@ -575,12 +577,7 @@ export class N2TreeNode extends CustomHTMLElement { this.rendered = false this.dragNode = null - this.elExpandToggle.addEventListener('click', event => { - if (this.node.hasChildren()) - this.expandNode(event) - else - _mbus.dispatch('TREE_NODE_SELECTED', this.node) - }) + this.elExpandToggle.addEventListener('click', () => this.sidebar.setNodeExpanded(this.node, !this.sidebar.getNodeExpanded(this.node.UUID))) this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node)) _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => { @@ -595,7 +592,6 @@ export class N2TreeNode extends CustomHTMLElement { this.elName.addEventListener('dragenter', event => this.dragEnter(event)) this.elName.addEventListener('dragleave', event => this.dragLeave(event)) }// }}} - dragStart(e) {// {{{ if (this.node.isModified()) { alert('Save note before moving it.') @@ -658,16 +654,6 @@ export class N2TreeNode extends CustomHTMLElement { _app.dragIcon.icon('') this.classList.remove('drag-target') }// }}} - - async expandNode(event) {// {{{ - const expanded = _app.sidebar.getNodeExpanded(this.node.UUID) - - if (event.shiftKey) { - _app.sidebar.recursiveExpand(this.node, !expanded) - } else { - _app.sidebar.setNodeExpanded(this.node, !expanded) - } - }// }}} async fetchChildren(force_fetch) {//{{{ if (this.children_populated && !force_fetch) return diff --git a/static/js/sync.mjs b/static/js/sync.mjs index daa603f..fe72c3f 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -90,7 +90,6 @@ export class Sync { nodeStore.setAppState('latest_sync_node', currMax) } catch (e) { console.error('sync node tree', e) - alert(e.message) } finally { syncEnd = Date.now() const duration = (syncEnd - syncStart) / 1000 @@ -158,8 +157,8 @@ export class Sync { _mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length }) } catch (e) { - console.error(e) - alert(e.message) + console.trace(e) + alert(e.error) return } } diff --git a/user.go b/user.go new file mode 100644 index 0000000..b1c2abf --- /dev/null +++ b/user.go @@ -0,0 +1,27 @@ +package main + +import ( + // External + "github.com/golang-jwt/jwt/v5" +) + +type UserSession struct { + UserID int + Username string + Password string + Name string + ClientUUID string +} + +func NewUser(claims jwt.MapClaims) (u UserSession) { + uid, _ := claims["uid"].(float64) + name, _ := claims["name"].(string) + username, _ := claims["login"].(string) + clientUUID, _ := claims["cid"].(string) + + u.UserID = int(uid) + u.Username = username + u.Name = name + u.ClientUUID = clientUUID + return +} diff --git a/user/pkg.go b/user/pkg.go deleted file mode 100644 index bcdfac8..0000000 --- a/user/pkg.go +++ /dev/null @@ -1,63 +0,0 @@ -package user - -import ( - // External - "github.com/golang-jwt/jwt/v5" - "github.com/jmoiron/sqlx" - - // Standard - "encoding/json" -) - -type User struct { - ID int - Username string - Name string - Preferences map[string]UserPreferences -} - -type UserSession struct { - UserID int - Username string - Password string - Name string - ClientUUID string - Db *sqlx.DB -} - -type UserPreferences struct { - DownloadImages bool - DownloadFiles bool -} - -func NewUser(claims jwt.MapClaims) (u UserSession) { - uid, _ := claims["uid"].(float64) - name, _ := claims["name"].(string) - username, _ := claims["login"].(string) - clientUUID, _ := claims["cid"].(string) - - u.UserID = int(uid) - u.Username = username - u.Name = name - u.ClientUUID = clientUUID - return -} - -func (u UserSession) Preferences() (prefs map[string]UserPreferences, err error) { - row := u.Db.QueryRow(`SELECT preferences FROM public.user WHERE id=$1`, u.UserID) - - var data []byte - err = row.Scan(&data) - if err != nil { - return - } - - err = json.Unmarshal(data, &prefs) - return -} - -func (u UserSession) SetPreferences(prefs map[string]UserPreferences) (err error) { - j, _ := json.Marshal(prefs) - _, err = u.Db.Exec(`UPDATE public.user SET preferences=$2 WHERE id=$1`, u.UserID, j) - return -} diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl index 2755aea..d54c71d 100644 --- a/views/pages/notes2.gotmpl +++ b/views/pages/notes2.gotmpl @@ -32,10 +32,9 @@ - - - - +
+ +
@@ -47,7 +46,6 @@ import {App} from "/js/{{ .VERSION }}/app.mjs" import {API} from 'api' import {Sync} from 'sync' - import { } from '/js/{{ .VERSION }}/page_preferences.mjs' import { } from '/js/{{ .VERSION }}/page_storage.mjs' import { } from '/js/{{ .VERSION }}/page_history.mjs' import { } from '/js/{{ .VERSION }}/file.mjs'