diff --git a/authentication/pkg.go b/authentication/pkg.go
index 9eb6245..c0b9a2e 100644
--- a/authentication/pkg.go
+++ b/authentication/pkg.go
@@ -8,6 +8,9 @@ import (
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
+ // Internal
+ appUser "notes2/user"
+
// Standard
"database/sql"
"encoding/hex"
@@ -27,12 +30,6 @@ 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
@@ -165,16 +162,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 User
+ User appUser.User
Token string
}{true, user, token})
w.Write(j)
} // }}}
-func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user User, err error) { // {{{
+func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user appUser.User, err error) { // {{{
var row *sql.Row
row = mngr.db.QueryRow(`
- SELECT id, username, name
+ SELECT id, username, name, preferences
FROM public.user
WHERE
LOWER(username) = LOWER($1) AND
@@ -183,13 +180,21 @@ func (mngr *Manager) Authenticate(username, password string) (authenticated bool
username,
password,
)
- err = row.Scan(&user.ID, &user.Username, &user.Name)
+ var data []byte
+ err = row.Scan(&user.ID, &user.Username, &user.Name, &data)
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
}
@@ -278,7 +283,7 @@ func (mngr *Manager) ChangePassword(username, currentPassword, newPassword strin
changed = (rowsAffected == 1)
return
} // }}}
-func (mngr *Manager) NewClientUUID(user User) (clientUUID string, err error) { // {{{
+func (mngr *Manager) NewClientUUID(user appUser.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 213d1ed..6e3cf94 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
// Internal
"notes2/authentication"
"notes2/html_template"
+ appUser "notes2/user"
"os"
// Standard
@@ -134,6 +135,8 @@ 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))
@@ -178,7 +181,7 @@ func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.Respon
}
// User object is added to the context for the next handler.
- user := NewUser(claims)
+ user := appUser.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)
@@ -266,7 +269,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 := getUser(r)
+ user := getUserSession(r)
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
offset, _ := strconv.Atoi(r.PathValue("offset"))
@@ -289,7 +292,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 := getUser(r)
+ user := getUserSession(r)
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
@@ -309,7 +312,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{
w.Write(j)
} // }}}
func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
- user := getUser(r)
+ user := getUserSession(r)
var err error
uuid := r.PathValue("uuid")
@@ -325,7 +328,7 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
})
} // }}}
func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
- user := getUser(r)
+ user := getUserSession(r)
var err error
uuid := r.PathValue("uuid")
@@ -348,7 +351,7 @@ func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
})
} // }}}
func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{
- user := getUser(r)
+ user := getUserSession(r)
var err error
uuid := r.PathValue("uuid")
@@ -360,12 +363,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 := getUser(r)
+ user := getUserSession(r)
body, _ := io.ReadAll(r.Body)
var request struct {
@@ -389,6 +392,47 @@ 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)
@@ -431,7 +475,8 @@ func changePassword(username string) { // {{{
fmt.Printf("\nPassword changed\n")
} // }}}
-func getUser(r *http.Request) UserSession { // {{{
- user, _ := r.Context().Value(CONTEXT_USER).(UserSession)
+func getUserSession(r *http.Request) appUser.UserSession { // {{{
+ user, _ := r.Context().Value(CONTEXT_USER).(appUser.UserSession)
+ user.Db = db
return user
} // }}}
diff --git a/sql/00010.sql b/sql/00010.sql
new file mode 100644
index 0000000..ecd8ab4
--- /dev/null
+++ b/sql/00010.sql
@@ -0,0 +1 @@
+ALTER TABLE public."user" ADD preferences jsonb DEFAULT '{}' NOT NULL;
diff --git a/static/css/notes2.css b/static/css/notes2.css
index a48a319..7fdea0b 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -73,9 +73,10 @@ button {
1fr;
}
- &.page-history {
+ /* The other pages just gets the whole page without dividing it up. */
+ &:not(.page-node) {
grid-template-areas:
- "tree-expander tree pad1 n2-pagehistory pad2"
+ "tree-expander tree pad1 n2-page pad2"
;
grid-template-columns:
@@ -245,7 +246,6 @@ button {
#notes2 {
&.page-node {
-
#page-root {
display: none;
}
@@ -260,7 +260,7 @@ button {
display: contents;
n2-pagestorage {
- grid-area: content;
+ grid-area: n2-page;
}
}
}
@@ -268,9 +268,14 @@ button {
&.page-history {
#page-history {
display: grid;
- grid-area: n2-pagehistory;
+ grid-area: n2-page;
+ }
+ }
- n2-pagehistory {}
+ &.page-preferences {
+ #page-preferences {
+ display: block;
+ grid-area: n2-page;
}
}
@@ -282,7 +287,6 @@ button {
#page-root {
display: contents !important;
}
-
}
}
diff --git a/static/images/icon_menu.svg b/static/images/icon_menu.svg
index e60bdee..cfdd1e8 100644
--- a/static/images/icon_menu.svg
+++ b/static/images/icon_menu.svg
@@ -2,18 +2,18 @@
+ transform="translate(-147.15925,-92.339586)">
menuhamburger
diff --git a/static/js/api.mjs b/static/js/api.mjs
index 3fff10a..26a19de 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) {
- return new Promise((resolve, reject) => {
+ try {
const body = JSON.stringify(request)
const headers = {}
@@ -12,33 +12,22 @@ export class API {
headers.Authorization = `Bearer ${token}`
}
- 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,
- }))
- })
+ 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, }})
+ }
}
static hasAuthenticationToken() {//{{{
diff --git a/static/js/app.mjs b/static/js/app.mjs
index c556341..90bad39 100644
--- a/static/js/app.mjs
+++ b/static/js/app.mjs
@@ -2,6 +2,7 @@ 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']
@@ -14,6 +15,8 @@ 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()
@@ -61,6 +64,11 @@ 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 => {
@@ -211,6 +219,12 @@ 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 2cec808..d1fb7ae 100644
--- a/static/js/lib/custom_html_element.mjs
+++ b/static/js/lib/custom_html_element.mjs
@@ -1,7 +1,17 @@
+/* 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 => {
@@ -9,6 +19,7 @@ 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
@@ -19,39 +30,22 @@ 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/page_preferences.mjs b/static/js/page_preferences.mjs
new file mode 100644
index 0000000..9655278
--- /dev/null
+++ b/static/js/page_preferences.mjs
@@ -0,0 +1,283 @@
+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 931a718..a007130 100644
--- a/static/js/page_storage.mjs
+++ b/static/js/page_storage.mjs
@@ -13,7 +13,10 @@ export class N2PageStorage extends CustomHTMLElement {
constructor() {
super()
- window._mbus.subscribe('SHOW_PAGE', () => this.render())
+ window._mbus.subscribe('SHOW_PAGE', event => {
+ if (event.detail.data?.page == 'storage')
+ this.render()
+ })
}
async render() {
const countNodes = await globalThis.nodeStore.nodeCount()
diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs
index 9c2b8c8..6cd5814 100644
--- a/static/js/sidebar.mjs
+++ b/static/js/sidebar.mjs
@@ -128,6 +128,7 @@ 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 })
@@ -248,8 +249,6 @@ 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)
@@ -258,38 +257,31 @@ 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
}
@@ -411,23 +403,26 @@ export class N2Sidebar extends CustomHTMLElement {
}//}}}
async navigateTop() {//{{{
const root = await nodeStore.get(ROOT_NODE)
- if (root.Children.length === 0)
- return
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: false, dontExpand: true })
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.UUID, dontPush: false, dontExpand: true })
}//}}}
async navigateBottom() {//{{{
- const root = await nodeStore.get(ROOT_NODE)
- if (root.Children.length === 0)
- return
+ const orphaned = await nodeStore.get(ORPHANED_NODE)
- const toplevel = root.Children[root.Children.length - 1]
+ if (!orphaned.hasChildren() || this.getNodeExpanded(orphaned.UUID)) {
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.UUID, dontPush: false, dontExpand: true })
+ return
+ }
+
+ /* TODO - fix this when orphaned nodes are implemented.
+ const toplevel = orphaned.Children[orphaned.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: root.Children[root.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.Children[orphaned.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
+ */
}//}}}
getParentWithNextSibling(node) {//{{{
@@ -445,11 +440,6 @@ export class N2Sidebar extends CustomHTMLElement {
return currNode
}//}}}
async recursiveExpand(node, state) {//{{{
- if (!state) {
- await this.setNodeExpanded(node, false)
- return
- }
-
if (state)
await this.setNodeExpanded(node, true)
@@ -459,6 +449,9 @@ export class N2Sidebar extends CustomHTMLElement {
for (const child of node.Children)
await this.recursiveExpand(child, state)
+
+ if (!state)
+ await this.setNodeExpanded(node, false)
}//}}}
async makeVisible(node, providedAncestors, dontExpand) {// {{{
const treenode = this.treeNodeComponents[node.UUID]
@@ -582,7 +575,12 @@ export class N2TreeNode extends CustomHTMLElement {
this.rendered = false
this.dragNode = null
- this.elExpandToggle.addEventListener('click', event => this.expandNode(event))
+ this.elExpandToggle.addEventListener('click', event => {
+ if (this.node.hasChildren())
+ this.expandNode(event)
+ else
+ _mbus.dispatch('TREE_NODE_SELECTED', this.node)
+ })
this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => {
diff --git a/static/js/sync.mjs b/static/js/sync.mjs
index fe72c3f..daa603f 100644
--- a/static/js/sync.mjs
+++ b/static/js/sync.mjs
@@ -90,6 +90,7 @@ 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
@@ -157,8 +158,8 @@ export class Sync {
_mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
} catch (e) {
- console.trace(e)
- alert(e.error)
+ console.error(e)
+ alert(e.message)
return
}
}
diff --git a/user.go b/user.go
deleted file mode 100644
index b1c2abf..0000000
--- a/user.go
+++ /dev/null
@@ -1,27 +0,0 @@
-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
new file mode 100644
index 0000000..bcdfac8
--- /dev/null
+++ b/user/pkg.go
@@ -0,0 +1,63 @@
+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 d54c71d..2755aea 100644
--- a/views/pages/notes2.gotmpl
+++ b/views/pages/notes2.gotmpl
@@ -32,9 +32,10 @@
-
-
-
+
+
+
+
@@ -46,6 +47,7 @@
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'