Compare commits
No commits in common. "main" and "v25" have entirely different histories.
17 changed files with 173 additions and 610 deletions
|
|
@ -8,9 +8,6 @@ import (
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
|
|
||||||
// Internal
|
|
||||||
appUser "notes2/user"
|
|
||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
|
@ -30,6 +27,12 @@ type Manager struct {
|
||||||
ExpireDays int
|
ExpireDays int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
func httpError(w http.ResponseWriter, err error) { // {{{
|
func httpError(w http.ResponseWriter, err error) { // {{{
|
||||||
j, _ := json.Marshal(struct {
|
j, _ := json.Marshal(struct {
|
||||||
OK bool
|
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")
|
mngr.log.Info("authentication", "username", request.Username, "status", "accepted")
|
||||||
j, _ := json.Marshal(struct {
|
j, _ := json.Marshal(struct {
|
||||||
OK bool
|
OK bool
|
||||||
User appUser.User
|
User User
|
||||||
Token string
|
Token string
|
||||||
}{true, user, token})
|
}{true, user, token})
|
||||||
w.Write(j)
|
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
|
var row *sql.Row
|
||||||
row = mngr.db.QueryRow(`
|
row = mngr.db.QueryRow(`
|
||||||
SELECT id, username, name, preferences
|
SELECT id, username, name
|
||||||
FROM public.user
|
FROM public.user
|
||||||
WHERE
|
WHERE
|
||||||
LOWER(username) = LOWER($1) AND
|
LOWER(username) = LOWER($1) AND
|
||||||
|
|
@ -180,21 +183,13 @@ func (mngr *Manager) Authenticate(username, password string) (authenticated bool
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
)
|
)
|
||||||
var data []byte
|
err = row.Scan(&user.ID, &user.Username, &user.Name)
|
||||||
err = row.Scan(&user.ID, &user.Username, &user.Name, &data)
|
|
||||||
if err != nil && err.Error() == "sql: no rows in result set" {
|
if err != nil && err.Error() == "sql: no rows in result set" {
|
||||||
err = nil
|
err = nil
|
||||||
authenticated = false
|
authenticated = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
authenticated = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(data, &user.Preferences)
|
|
||||||
if err != nil {
|
|
||||||
authenticated = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,7 +278,7 @@ func (mngr *Manager) ChangePassword(username, currentPassword, newPassword strin
|
||||||
changed = (rowsAffected == 1)
|
changed = (rowsAffected == 1)
|
||||||
return
|
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.
|
// Each client session has its own UUID.
|
||||||
// Loop through until a unique one is established.
|
// Loop through until a unique one is established.
|
||||||
var proposedClientUUID string
|
var proposedClientUUID string
|
||||||
|
|
|
||||||
65
main.go
65
main.go
|
|
@ -4,7 +4,6 @@ import (
|
||||||
// Internal
|
// Internal
|
||||||
"notes2/authentication"
|
"notes2/authentication"
|
||||||
"notes2/html_template"
|
"notes2/html_template"
|
||||||
appUser "notes2/user"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
|
|
@ -24,7 +23,7 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "v29"
|
const VERSION = "v25"
|
||||||
const CONTEXT_USER = 1
|
const CONTEXT_USER = 1
|
||||||
const SYNC_PAGINATION = 200
|
const SYNC_PAGINATION = 200
|
||||||
|
|
||||||
|
|
@ -135,8 +134,6 @@ func main() { // {{{
|
||||||
http.HandleFunc("/offline", pageOffline)
|
http.HandleFunc("/offline", pageOffline)
|
||||||
|
|
||||||
http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler)
|
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/count/{sequence}", authenticated(actionSyncFromServerCount))
|
||||||
http.HandleFunc("/sync/from_server/{sequence}/{offset}", authenticated(actionSyncFromServer))
|
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 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))
|
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)
|
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
|
// The purpose of the Client UUID is to avoid
|
||||||
// sending nodes back once again to a client that
|
// sending nodes back once again to a client that
|
||||||
// just created or modified it.
|
// just created or modified it.
|
||||||
user := getUserSession(r)
|
user := getUser(r)
|
||||||
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
||||||
offset, _ := strconv.Atoi(r.PathValue("offset"))
|
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
|
// The purpose of the Client UUID is to avoid
|
||||||
// sending nodes back once again to a client that
|
// sending nodes back once again to a client that
|
||||||
// just created or modified it.
|
// just created or modified it.
|
||||||
user := getUserSession(r)
|
user := getUser(r)
|
||||||
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
||||||
|
|
||||||
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
|
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
|
||||||
|
|
@ -312,7 +309,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
w.Write(j)
|
w.Write(j)
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
user := getUserSession(r)
|
user := getUser(r)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
uuid := r.PathValue("uuid")
|
uuid := r.PathValue("uuid")
|
||||||
|
|
@ -328,7 +325,7 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
user := getUserSession(r)
|
user := getUser(r)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
uuid := r.PathValue("uuid")
|
uuid := r.PathValue("uuid")
|
||||||
|
|
@ -351,7 +348,7 @@ func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{
|
func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
user := getUserSession(r)
|
user := getUser(r)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
uuid := r.PathValue("uuid")
|
uuid := r.PathValue("uuid")
|
||||||
|
|
@ -368,7 +365,7 @@ func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
user := getUserSession(r)
|
user := getUser(r)
|
||||||
|
|
||||||
body, _ := io.ReadAll(r.Body)
|
body, _ := io.ReadAll(r.Body)
|
||||||
var request struct {
|
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) { // {{{
|
func createNewUser(username string) { // {{{
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
|
@ -475,8 +431,7 @@ func changePassword(username string) { // {{{
|
||||||
|
|
||||||
fmt.Printf("\nPassword changed\n")
|
fmt.Printf("\nPassword changed\n")
|
||||||
} // }}}
|
} // }}}
|
||||||
func getUserSession(r *http.Request) appUser.UserSession { // {{{
|
func getUser(r *http.Request) UserSession { // {{{
|
||||||
user, _ := r.Context().Value(CONTEXT_USER).(appUser.UserSession)
|
user, _ := r.Context().Value(CONTEXT_USER).(UserSession)
|
||||||
user.Db = db
|
|
||||||
return user
|
return user
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE public."user" ADD preferences jsonb DEFAULT '{}' NOT NULL;
|
|
||||||
|
|
@ -102,11 +102,6 @@
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&.copy {
|
|
||||||
border: var(--markdown-copy-border);
|
|
||||||
background-color: var(--markdown-copy-background);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
|
@ -116,14 +111,6 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
||||||
&.copy {
|
|
||||||
border: var(--markdown-copy-border);
|
|
||||||
background-color: var(--markdown-copy-background);
|
|
||||||
code {
|
|
||||||
background-color: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
code {
|
||||||
border: unset;
|
border: unset;
|
||||||
padding: unset;
|
padding: unset;
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@
|
||||||
--menu-item-hover-color: #f4f4f4;
|
--menu-item-hover-color: #f4f4f4;
|
||||||
|
|
||||||
--font-monospace: "Liberation Mono", monospace;
|
--font-monospace: "Liberation Mono", monospace;
|
||||||
|
|
||||||
--markdown-copy-border: 1px solid #0a0;
|
|
||||||
--markdown-copy-background: #e3f4d7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
@ -73,10 +70,9 @@ button {
|
||||||
1fr;
|
1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The other pages just gets the whole page without dividing it up. */
|
&.page-history {
|
||||||
&:not(.page-node) {
|
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"tree-expander tree pad1 n2-page pad2"
|
"tree-expander tree pad1 n2-pagehistory pad2"
|
||||||
;
|
;
|
||||||
|
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
|
|
@ -246,6 +242,7 @@ button {
|
||||||
|
|
||||||
#notes2 {
|
#notes2 {
|
||||||
&.page-node {
|
&.page-node {
|
||||||
|
|
||||||
#page-root {
|
#page-root {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +257,7 @@ button {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
||||||
n2-pagestorage {
|
n2-pagestorage {
|
||||||
grid-area: n2-page;
|
grid-area: content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -268,14 +265,9 @@ button {
|
||||||
&.page-history {
|
&.page-history {
|
||||||
#page-history {
|
#page-history {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-area: n2-page;
|
grid-area: n2-pagehistory;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.page-preferences {
|
n2-pagehistory {}
|
||||||
#page-preferences {
|
|
||||||
display: block;
|
|
||||||
grid-area: n2-page;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,6 +279,7 @@ button {
|
||||||
#page-root {
|
#page-root {
|
||||||
display: contents !important;
|
display: contents !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,18 @@
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="12"
|
width="26.666645"
|
||||||
height="23.999981"
|
height="24"
|
||||||
viewBox="0 0 3.1750001 6.349995"
|
viewBox="0 0 7.0555498 6.35"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||||
sodipodi:docname="icon_menu.svg"
|
sodipodi:docname="icon_menu.svg"
|
||||||
xml:space="preserve"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
id="namedview1"
|
id="namedview1"
|
||||||
pagecolor="#ffffff"
|
pagecolor="#ffffff"
|
||||||
bordercolor="#000000"
|
bordercolor="#000000"
|
||||||
|
|
@ -23,34 +23,29 @@
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:zoom="11.859035"
|
inkscape:zoom="2.096401"
|
||||||
inkscape:cx="8.6010372"
|
inkscape:cx="10.255672"
|
||||||
inkscape:cy="17.32856"
|
inkscape:cy="9.0631517"
|
||||||
inkscape:window-width="2190"
|
inkscape:window-width="1916"
|
||||||
inkscape:window-height="1401"
|
inkscape:window-height="1041"
|
||||||
inkscape:window-x="1463"
|
inkscape:window-x="1920"
|
||||||
inkscape:window-y="18"
|
inkscape:window-y="1080"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1" /><defs
|
inkscape:current-layer="layer1" />
|
||||||
id="defs1" /><g
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
inkscape:label="Layer 1"
|
inkscape:label="Layer 1"
|
||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer1"
|
id="layer1"
|
||||||
transform="translate(-147.15925,-92.339586)"><title
|
transform="translate(-146.57917,-92.339583)">
|
||||||
id="title1">menu</title><title
|
<title
|
||||||
id="title1-6">hamburger</title><circle
|
id="title1">menu</title>
|
||||||
style="fill:#000000;stroke:none;stroke-width:0.264583"
|
<title
|
||||||
id="path3"
|
id="title1-6">hamburger</title>
|
||||||
cx="149.55338"
|
<path
|
||||||
cy="93.120461"
|
d="m 153.63472,95.867362 c 0,0.391582 -0.31398,0.705554 -0.70555,0.705554 h -5.64445 c -0.38806,0 -0.70555,-0.313972 -0.70555,-0.705554 0,-0.391584 0.31749,-0.705556 0.70555,-0.705556 h 3.175 l 0.88194,0.705556 0.88195,-0.705556 h 0.70556 c 0.39157,0 0.70555,0.3175 0.70555,0.705556 m -3.52778,-3.527779 c -3.175,0 -3.175,2.116667 -3.175,2.116667 h 6.35 c 0,0 0,-2.116667 -3.175,-2.116667 m -3.175,5.291667 c 0,0.585612 0.47272,1.058333 1.05834,1.058333 h 4.23333 c 0.58561,0 1.05833,-0.472721 1.05833,-1.058333 v -0.352778 h -6.35 z"
|
||||||
r="0.78087437" /><circle
|
id="path1-2"
|
||||||
style="fill:#000000;stroke:none;stroke-width:0.264583"
|
style="stroke-width:0.352777" />
|
||||||
id="circle4"
|
</g>
|
||||||
cx="149.55338"
|
</svg>
|
||||||
cy="97.908707"
|
|
||||||
r="0.78087437" /><circle
|
|
||||||
style="fill:#000000;stroke:none;stroke-width:0.264583"
|
|
||||||
id="circle5"
|
|
||||||
cx="149.55338"
|
|
||||||
cy="95.514587"
|
|
||||||
r="0.78087437" /></g></svg>
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,7 +1,7 @@
|
||||||
export class API {
|
export class API {
|
||||||
// query resolves into the JSON data produced by the application, or an exception with 'type' and 'error' properties.
|
// query resolves into the JSON data produced by the application, or an exception with 'type' and 'error' properties.
|
||||||
static async query(method, path, request) {
|
static async query(method, path, request) {
|
||||||
try {
|
return new Promise((resolve, reject) => {
|
||||||
const body = JSON.stringify(request)
|
const body = JSON.stringify(request)
|
||||||
const headers = {}
|
const headers = {}
|
||||||
|
|
||||||
|
|
@ -12,22 +12,33 @@ export class API {
|
||||||
headers.Authorization = `Bearer ${token}`
|
headers.Authorization = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(path, { method, headers, body })
|
fetch(path, { method, headers, body })
|
||||||
|
.then(response => {
|
||||||
// An HTTP communication level error occured.
|
// An HTTP communication level error occured.
|
||||||
if (!res.ok || res.status != 200)
|
if (!response.ok || response.status != 200)
|
||||||
throw new Error('HTTP error', { cause: { type: 'http', error: res, }})
|
return reject({
|
||||||
|
type: 'http',
|
||||||
|
error: response,
|
||||||
|
})
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
// Application level response are handled here.
|
// Application level response are handled here.
|
||||||
const json = await res.json()
|
|
||||||
if (!json.OK)
|
if (!json.OK)
|
||||||
throw new Error(json.Error, { cause: { type: 'application', application: json, }})
|
return reject({
|
||||||
|
type: 'application',
|
||||||
return json
|
error: json.Error,
|
||||||
|
application: json,
|
||||||
} catch (err) {
|
})
|
||||||
|
resolve(json)
|
||||||
|
})
|
||||||
|
.catch(err =>
|
||||||
// Catch any other errors from fetch.
|
// Catch any other errors from fetch.
|
||||||
throw new Error(err.message, { cause: { type: 'http', error: err, }})
|
reject({
|
||||||
}
|
type: 'http',
|
||||||
|
error: err,
|
||||||
|
}))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static hasAuthenticationToken() {//{{{
|
static hasAuthenticationToken() {//{{{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { ROOT_NODE } from 'node_store'
|
||||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||||
import { N2Sidebar } from 'sidebar'
|
import { N2Sidebar } from 'sidebar'
|
||||||
import { Node } from 'node'
|
import { Node } from 'node'
|
||||||
import { N2PreferenceSet } from './page_preferences.mjs'
|
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
static PAGES = ['node', 'history', 'storage']
|
static PAGES = ['node', 'history', 'storage']
|
||||||
|
|
@ -15,8 +14,6 @@ export class App {
|
||||||
this.nodeUI = document.getElementById('note')
|
this.nodeUI = document.getElementById('note')
|
||||||
this.dragIcon = new N2DragIcon()
|
this.dragIcon = new N2DragIcon()
|
||||||
|
|
||||||
this.preferences = this.getPreferences()
|
|
||||||
|
|
||||||
this.sidebar.render().then(sidebar => {
|
this.sidebar.render().then(sidebar => {
|
||||||
document.getElementById('tree').append(sidebar)
|
document.getElementById('tree').append(sidebar)
|
||||||
document.getElementById('tree-nodes')?.focus()
|
document.getElementById('tree-nodes')?.focus()
|
||||||
|
|
@ -64,11 +61,6 @@ export class App {
|
||||||
classList.add('page-' + page)
|
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('keydown', event => this.keyHandler(event))
|
||||||
window.addEventListener('popstate', event => this.popState(event))
|
window.addEventListener('popstate', event => this.popState(event))
|
||||||
document.getElementById('notes2').addEventListener('click', event => {
|
document.getElementById('notes2').addEventListener('click', event => {
|
||||||
|
|
@ -219,12 +211,6 @@ export class App {
|
||||||
let classList = document.querySelector('#main-page').classList
|
let classList = document.querySelector('#main-page').classList
|
||||||
return classList.contains(page)
|
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 {
|
class N2Crumbs extends CustomHTMLElement {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
export class CustomHTMLElement extends HTMLElement {
|
||||||
constructor(useShadow) {// {{{
|
constructor(useShadow) {// {{{
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this._fields = new Map()
|
|
||||||
|
|
||||||
const workOn = useShadow ? this.attachShadow({ mode: 'open' }) : this
|
const workOn = useShadow ? this.attachShadow({ mode: 'open' }) : this
|
||||||
workOn.appendChild(this.constructor.tmpl.content.cloneNode(true))
|
workOn.appendChild(this.constructor.tmpl.content.cloneNode(true))
|
||||||
workOn.querySelectorAll('*').forEach(el => {
|
workOn.querySelectorAll('*').forEach(el => {
|
||||||
|
|
@ -19,7 +9,6 @@ export class CustomHTMLElement extends HTMLElement {
|
||||||
if (field !== undefined) {
|
if (field !== undefined) {
|
||||||
const fieldName = this.toElementName('field', field)
|
const fieldName = this.toElementName('field', field)
|
||||||
this[fieldName] = el
|
this[fieldName] = el
|
||||||
this._fields.set(this.toElementName('', field), el)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = el.dataset.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) {// {{{
|
toElementName(prefix, str) {// {{{
|
||||||
str = prefix + '-' + str
|
str = prefix + '-' + str
|
||||||
return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', ''))
|
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)
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,6 @@ export class MarkedPosition {
|
||||||
constructor() {// {{{
|
constructor() {// {{{
|
||||||
window.marked_setpos = (event) => this.setpos(event)
|
window.marked_setpos = (event) => this.setpos(event)
|
||||||
window.marked_changecheckbox = (event) => this.changecheckbox(event)
|
window.marked_changecheckbox = (event) => this.changecheckbox(event)
|
||||||
window.marked_copy_to_clipboard = (event, tagname) => this.copy_to_clipboard(event, tagname)
|
|
||||||
this.render()
|
this.render()
|
||||||
}// }}}
|
}// }}}
|
||||||
setpos(event) {// {{{
|
setpos(event) {// {{{
|
||||||
|
|
@ -120,32 +119,8 @@ export class MarkedPosition {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
async copy_to_clipboard(event, tagname) {// {{{
|
|
||||||
if (!event.shiftKey)
|
|
||||||
return
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Stop text selections on the page to the mouse pointer.
|
|
||||||
// Old selections are remove as well to give a cleaner view
|
|
||||||
// of the copied text/highlighting.
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
window.getSelection().removeAllRanges()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to copy: ', err)
|
|
||||||
alert('Failed to copy: ', err)
|
|
||||||
}
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
render() {// {{{
|
render() {// {{{
|
||||||
|
const markedObject = this
|
||||||
this.marked = new Marked()
|
this.marked = new Marked()
|
||||||
this.marked.use(markedTokenPosition())
|
this.marked.use(markedTokenPosition())
|
||||||
this.marked.use({
|
this.marked.use({
|
||||||
|
|
@ -190,12 +165,12 @@ export class MarkedPosition {
|
||||||
const code = token.text.replace(other.endingNewline, '') + '\n'
|
const code = token.text.replace(other.endingNewline, '') + '\n'
|
||||||
|
|
||||||
if (!langString) {
|
if (!langString) {
|
||||||
return `<pre ondblclick="marked_setpos(event)" onmousedown="marked_copy_to_clipboard(event, 'pre')" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code>`
|
return `<pre ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code>`
|
||||||
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
||||||
+ '</code></pre>\n'
|
+ '</code></pre>\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<pre ondblclick="marked_setpos(event)" onmousedown="marked_copy_to_clipboard(event, 'pre')" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code class="language-`
|
return `<pre ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code class="language-`
|
||||||
+ escapeHtmlEntities(langString)
|
+ escapeHtmlEntities(langString)
|
||||||
+ '">'
|
+ '">'
|
||||||
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
||||||
|
|
@ -285,7 +260,7 @@ export class MarkedPosition {
|
||||||
},
|
},
|
||||||
|
|
||||||
codespan(token) {
|
codespan(token) {
|
||||||
return `<code ondblclick="marked_setpos(event)" onmousedown="marked_copy_to_clipboard(event, 'code')" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${escapeHtmlEntities(token.text, true)}</code>`
|
return `<code ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${escapeHtmlEntities(token.text, true)}</code>`
|
||||||
},
|
},
|
||||||
|
|
||||||
br(token) {
|
br(token) {
|
||||||
|
|
|
||||||
|
|
@ -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 = `
|
|
||||||
<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(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 = `
|
|
||||||
<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)
|
|
||||||
|
|
@ -13,10 +13,7 @@ export class N2PageStorage extends CustomHTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
window._mbus.subscribe('SHOW_PAGE', event => {
|
window._mbus.subscribe('SHOW_PAGE', () => this.render())
|
||||||
if (event.detail.data?.page == 'storage')
|
|
||||||
this.render()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
async render() {
|
async render() {
|
||||||
const countNodes = await globalThis.nodeStore.nodeCount()
|
const countNodes = await globalThis.nodeStore.nodeCount()
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,6 @@ export class N2Sidebar extends CustomHTMLElement {
|
||||||
this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search'))
|
this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search'))
|
||||||
this.elSync.addEventListener('click', () => _sync.run())
|
this.elSync.addEventListener('click', () => _sync.run())
|
||||||
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
|
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 => {
|
this.elHideTree.addEventListener('click', event => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
_mbus.dispatch('TREE_EXPANSION', { expand: false })
|
_mbus.dispatch('TREE_EXPANSION', { expand: false })
|
||||||
|
|
@ -198,8 +197,9 @@ export class N2Sidebar extends CustomHTMLElement {
|
||||||
this.expandedNodes[UUID] = false
|
this.expandedNodes[UUID] = false
|
||||||
return this.expandedNodes[UUID]
|
return this.expandedNodes[UUID]
|
||||||
}//}}}
|
}//}}}
|
||||||
async setNodeExpanded(node, value) {//{{{
|
setNodeExpanded(node, value) {//{{{
|
||||||
let expanded = this.expandedNodes[node.UUID]
|
let expanded = this.expandedNodes[node.UUID]
|
||||||
|
|
||||||
if (expanded === undefined) {
|
if (expanded === undefined) {
|
||||||
this.expandedNodes[node.UUID] = false
|
this.expandedNodes[node.UUID] = false
|
||||||
expanded = false
|
expanded = false
|
||||||
|
|
@ -249,6 +249,8 @@ export class N2Sidebar extends CustomHTMLElement {
|
||||||
// Holding shift down does it recursively.
|
// Holding shift down does it recursively.
|
||||||
case Space:
|
case Space:
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
|
if (n.UUID === ROOT_NODE)
|
||||||
|
return
|
||||||
const expanded = this.getNodeExpanded(n.UUID)
|
const expanded = this.getNodeExpanded(n.UUID)
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
this.recursiveExpand(n, !expanded)
|
this.recursiveExpand(n, !expanded)
|
||||||
|
|
@ -257,31 +259,38 @@ export class N2Sidebar extends CustomHTMLElement {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'g':
|
||||||
case 'Home':
|
case 'Home':
|
||||||
this.navigateTop()
|
this.navigateTop()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'G':
|
||||||
case 'End':
|
case 'End':
|
||||||
this.navigateBottom()
|
this.navigateBottom()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'j':
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
await this.navigateDown(this.selectedNode)
|
await this.navigateDown(this.selectedNode)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'k':
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
await this.navigateUp(this.selectedNode)
|
await this.navigateUp(this.selectedNode)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'h':
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
await this.navigateLeft(this.selectedNode)
|
await this.navigateLeft(this.selectedNode)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'l':
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
await this.navigateRight(this.selectedNode)
|
await this.navigateRight(this.selectedNode)
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
// nonsole.log(event.key)
|
||||||
handled = false
|
handled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,26 +412,23 @@ export class N2Sidebar extends CustomHTMLElement {
|
||||||
}//}}}
|
}//}}}
|
||||||
async navigateTop() {//{{{
|
async navigateTop() {//{{{
|
||||||
const root = await nodeStore.get(ROOT_NODE)
|
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() {//{{{
|
async navigateBottom() {//{{{
|
||||||
const orphaned = await nodeStore.get(ORPHANED_NODE)
|
const root = await nodeStore.get(ROOT_NODE)
|
||||||
|
if (root.Children.length === 0)
|
||||||
if (!orphaned.hasChildren() || this.getNodeExpanded(orphaned.UUID)) {
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
/* TODO - fix this when orphaned nodes are implemented.
|
const toplevel = root.Children[root.Children.length - 1]
|
||||||
const toplevel = orphaned.Children[orphaned.Children.length - 1]
|
|
||||||
const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID)
|
const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID)
|
||||||
|
|
||||||
if (toplevelExpanded) {
|
if (toplevelExpanded) {
|
||||||
const lastnode = this.getLastExpandedNode(toplevel)
|
const lastnode = this.getLastExpandedNode(toplevel)
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: false, dontExpand: true })
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: false, dontExpand: true })
|
||||||
} else
|
} 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) {//{{{
|
getParentWithNextSibling(node) {//{{{
|
||||||
|
|
@ -443,10 +449,6 @@ export class N2Sidebar extends CustomHTMLElement {
|
||||||
if (state)
|
if (state)
|
||||||
await this.setNodeExpanded(node, true)
|
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)
|
for (const child of node.Children)
|
||||||
await this.recursiveExpand(child, state)
|
await this.recursiveExpand(child, state)
|
||||||
|
|
||||||
|
|
@ -575,12 +577,7 @@ export class N2TreeNode extends CustomHTMLElement {
|
||||||
this.rendered = false
|
this.rendered = false
|
||||||
this.dragNode = null
|
this.dragNode = null
|
||||||
|
|
||||||
this.elExpandToggle.addEventListener('click', event => {
|
this.elExpandToggle.addEventListener('click', () => this.sidebar.setNodeExpanded(this.node, !this.sidebar.getNodeExpanded(this.node.UUID)))
|
||||||
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))
|
this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
|
||||||
|
|
||||||
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => {
|
_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('dragenter', event => this.dragEnter(event))
|
||||||
this.elName.addEventListener('dragleave', event => this.dragLeave(event))
|
this.elName.addEventListener('dragleave', event => this.dragLeave(event))
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
dragStart(e) {// {{{
|
dragStart(e) {// {{{
|
||||||
if (this.node.isModified()) {
|
if (this.node.isModified()) {
|
||||||
alert('Save note before moving it.')
|
alert('Save note before moving it.')
|
||||||
|
|
@ -658,16 +654,6 @@ export class N2TreeNode extends CustomHTMLElement {
|
||||||
_app.dragIcon.icon('')
|
_app.dragIcon.icon('')
|
||||||
this.classList.remove('drag-target')
|
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) {//{{{
|
async fetchChildren(force_fetch) {//{{{
|
||||||
if (this.children_populated && !force_fetch)
|
if (this.children_populated && !force_fetch)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ export class Sync {
|
||||||
nodeStore.setAppState('latest_sync_node', currMax)
|
nodeStore.setAppState('latest_sync_node', currMax)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('sync node tree', e)
|
console.error('sync node tree', e)
|
||||||
alert(e.message)
|
|
||||||
} finally {
|
} finally {
|
||||||
syncEnd = Date.now()
|
syncEnd = Date.now()
|
||||||
const duration = (syncEnd - syncStart) / 1000
|
const duration = (syncEnd - syncStart) / 1000
|
||||||
|
|
@ -158,8 +157,8 @@ export class Sync {
|
||||||
_mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
|
_mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.trace(e)
|
||||||
alert(e.message)
|
alert(e.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
user.go
Normal file
27
user.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
63
user/pkg.go
63
user/pkg.go
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -32,10 +32,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- History -->
|
<!-- History -->
|
||||||
<n2-pagehistory id="page-history"></n2-pagehistory>
|
<div id="page-history">
|
||||||
|
<n2-pagehistory></n2-pagehistory>
|
||||||
<!-- Preferences -->
|
</div>
|
||||||
<n2-pagepreferences id="page-preferences"></n2-pagepreferences>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n2-syncprogress></n2-syncprogress>
|
<n2-syncprogress></n2-syncprogress>
|
||||||
|
|
@ -47,7 +46,6 @@
|
||||||
import {App} from "/js/{{ .VERSION }}/app.mjs"
|
import {App} from "/js/{{ .VERSION }}/app.mjs"
|
||||||
import {API} from 'api'
|
import {API} from 'api'
|
||||||
import {Sync} from 'sync'
|
import {Sync} from 'sync'
|
||||||
import { } from '/js/{{ .VERSION }}/page_preferences.mjs'
|
|
||||||
import { } from '/js/{{ .VERSION }}/page_storage.mjs'
|
import { } from '/js/{{ .VERSION }}/page_storage.mjs'
|
||||||
import { } from '/js/{{ .VERSION }}/page_history.mjs'
|
import { } from '/js/{{ .VERSION }}/page_history.mjs'
|
||||||
import { } from '/js/{{ .VERSION }}/file.mjs'
|
import { } from '/js/{{ .VERSION }}/file.mjs'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue