Prefs managing mostly done
This commit is contained in:
parent
81d02b82dc
commit
74851b9c4d
6 changed files with 344 additions and 51 deletions
48
main.go
48
main.go
|
|
@ -135,7 +135,8 @@ func main() { // {{{
|
|||
http.HandleFunc("/offline", pageOffline)
|
||||
|
||||
http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler)
|
||||
http.HandleFunc("/user/preferences", authenticated(actionUserPreferences))
|
||||
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))
|
||||
|
|
@ -268,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"))
|
||||
|
||||
|
|
@ -291,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)
|
||||
|
|
@ -311,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")
|
||||
|
|
@ -327,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")
|
||||
|
|
@ -350,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")
|
||||
|
|
@ -367,7 +368,7 @@ func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
})
|
||||
} // }}}
|
||||
func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
user := getUser(r)
|
||||
user := getUserSession(r)
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var request struct {
|
||||
|
|
@ -391,8 +392,8 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
})
|
||||
} // }}}
|
||||
|
||||
func actionUserPreferences(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
user := getUser(r)
|
||||
func actionUserGetPreferences(w http.ResponseWriter, r *http.Request) { // {{{
|
||||
user := getUserSession(r)
|
||||
prefs, err := user.Preferences()
|
||||
if err != nil {
|
||||
httpError(w, err)
|
||||
|
|
@ -404,6 +405,33 @@ func actionUserPreferences(w http.ResponseWriter, r *http.Request) { // {{{
|
|||
"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)
|
||||
|
|
@ -447,7 +475,7 @@ func changePassword(username string) { // {{{
|
|||
|
||||
fmt.Printf("\nPassword changed\n")
|
||||
} // }}}
|
||||
func getUser(r *http.Request) appUser.UserSession { // {{{
|
||||
func getUserSession(r *http.Request) appUser.UserSession { // {{{
|
||||
user, _ := r.Context().Value(CONTEXT_USER).(appUser.UserSession)
|
||||
user.Db = db
|
||||
return user
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ button {
|
|||
|
||||
&.page-preferences {
|
||||
#page-preferences {
|
||||
display: grid !important;
|
||||
display: block;
|
||||
grid-area: n2-page;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}// }}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,28 +5,279 @@ export class N2PagePreferences extends CustomHTMLElement {
|
|||
static {// {{{
|
||||
this.tmpl = document.createElement('template')
|
||||
this.tmpl.innerHTML = `
|
||||
<style>
|
||||
.el-sets {
|
||||
display: grid;
|
||||
grid-template-columns: min-content;
|
||||
grid-gap: 32px;
|
||||
}
|
||||
|
||||
:host > div {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.dev-pref-set {
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content;
|
||||
grid-gap: 16px;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<h1>Preferences</h1>
|
||||
|
||||
<div>Changes preferences to not download images or files on the device doesn't remove the already downloaded data.</div>
|
||||
|
||||
<div class="dev-pref-set">
|
||||
<div>Device preference set</div>
|
||||
<select data-el="dev-preference-set"></select>
|
||||
</div>
|
||||
|
||||
<div data-el="sets"></div>
|
||||
|
||||
<button data-el="new-set">New set</button>
|
||||
<button data-el="save" disabled>Save</button>
|
||||
`
|
||||
}// }}}
|
||||
constructor() {// {{{
|
||||
super()
|
||||
window._mbus.subscribe('SHOW_PAGE', event => {
|
||||
if (event.detail.data?.page == 'preferences')
|
||||
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() {// {{{
|
||||
}// }}}
|
||||
getPreferences() {
|
||||
API.query('GET', '/user/preferences')
|
||||
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.
|
||||
class Preferences {
|
||||
constructor(name, data) {
|
||||
export class N2PreferenceSet extends CustomHTMLElement {
|
||||
static {// {{{
|
||||
this.tmpl = document.createElement('template')
|
||||
this.tmpl.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
border: 1px solid var(--line-color);
|
||||
padding: 16px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
justify-items: start;
|
||||
align-items: center;
|
||||
grid-gap: 8px 16px;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
.header {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
|
||||
.el-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 32px;
|
||||
cursor: pointer;
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
.el-delete {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="header">
|
||||
<div data-el="name"></div>
|
||||
<div data-el="delete">✘</div>
|
||||
</div>
|
||||
|
||||
<div><label for="download-images">Download images on device</label></div>
|
||||
<input data-field="download-images" type="checkbox" id="download-images">
|
||||
|
||||
<div><label for="download-files">Download files on device</label></div>
|
||||
<input data-field="download-files" type="checkbox" id="download-files">
|
||||
`
|
||||
}// }}}
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -55,3 +55,9 @@ func (u UserSession) Preferences() (prefs map[string]UserPreferences, err error)
|
|||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue