Compare commits
No commits in common. "main" and "v6" have entirely different histories.
|
|
@ -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
|
||||||
|
|
|
||||||
113
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 = "v6"
|
||||||
const CONTEXT_USER = 1
|
const CONTEXT_USER = 1
|
||||||
const SYNC_PAGINATION = 200
|
const SYNC_PAGINATION = 200
|
||||||
|
|
||||||
|
|
@ -135,16 +134,12 @@ 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))
|
||||||
http.HandleFunc("/sync/to_server", authenticated(actionSyncToServer))
|
http.HandleFunc("/sync/to_server", authenticated(actionSyncToServer))
|
||||||
|
|
||||||
http.HandleFunc("/node/retrieve/{uuid}", authenticated(actionNodeRetrieve))
|
http.HandleFunc("/node/retrieve/{uuid}", authenticated(actionNodeRetrieve))
|
||||||
http.HandleFunc("/node/history/retrieve/{uuid}/{offset}", authenticated(actionNodeHistoryRetrieve))
|
|
||||||
http.HandleFunc("/node/history/count/{uuid}", authenticated(actionNodeHistoryCount))
|
|
||||||
|
|
||||||
http.HandleFunc("/service_worker.js", pageServiceWorker)
|
http.HandleFunc("/service_worker.js", pageServiceWorker)
|
||||||
|
|
||||||
|
|
@ -181,7 +176,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 +264,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"))
|
||||||
|
|
||||||
|
|
@ -280,6 +275,12 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq)
|
||||||
|
foo, _ := json.Marshal(nodes)
|
||||||
|
os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644)
|
||||||
|
*/
|
||||||
|
|
||||||
j, _ := json.Marshal(struct {
|
j, _ := json.Marshal(struct {
|
||||||
OK bool
|
OK bool
|
||||||
Nodes []Node
|
Nodes []Node
|
||||||
|
|
@ -292,7 +293,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 +313,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")
|
||||||
|
|
@ -327,48 +328,8 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
"Node": node,
|
"Node": node,
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
|
|
||||||
user := getUserSession(r)
|
|
||||||
var err error
|
|
||||||
|
|
||||||
uuid := r.PathValue("uuid")
|
|
||||||
offset, err := strconv.Atoi(r.PathValue("offset"))
|
|
||||||
if err != nil {
|
|
||||||
responseError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes, hasMore, err := RetrieveNodeHistory(user.UserID, uuid, offset)
|
|
||||||
if err != nil {
|
|
||||||
responseError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseData(w, map[string]any{
|
|
||||||
"OK": true,
|
|
||||||
"Nodes": nodes,
|
|
||||||
"HasMore": hasMore,
|
|
||||||
})
|
|
||||||
} // }}}
|
|
||||||
func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{
|
|
||||||
user := getUserSession(r)
|
|
||||||
var err error
|
|
||||||
|
|
||||||
uuid := r.PathValue("uuid")
|
|
||||||
|
|
||||||
count, err := RetrieveNodeHistoryCount(user.UserID, uuid)
|
|
||||||
if err != nil {
|
|
||||||
responseError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseData(w, map[string]any{
|
|
||||||
"OK": true,
|
|
||||||
"Count": count,
|
|
||||||
})
|
|
||||||
} // }}}
|
|
||||||
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 {
|
||||||
|
|
@ -380,50 +341,9 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`CALL add_nodes($1, $2::uuid, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
|
_, err = db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
|
||||||
if err != nil {
|
|
||||||
Log.Error("sync", "error", err, "user_id", user.UserID, "client_uuid", user.ClientUUID, "node_data", request.NodeData)
|
|
||||||
httpError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseData(w, map[string]any{
|
|
||||||
"OK": true,
|
|
||||||
})
|
|
||||||
} // }}}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
|
Log.Error("sync", "error", err)
|
||||||
httpError(w, err)
|
httpError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -475,8 +395,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
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
|
||||||
89
node.go
|
|
@ -3,8 +3,8 @@ package main
|
||||||
import (
|
import (
|
||||||
// External
|
// External
|
||||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
werr "git.gibonuddevalla.se/go/wrappederror"
|
||||||
"github.com/derektata/lorem/ipsum"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/derektata/lorem/ipsum"
|
||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
@ -44,7 +44,6 @@ type Node struct {
|
||||||
UUID string
|
UUID string
|
||||||
UserID int `db:"user_id"`
|
UserID int `db:"user_id"`
|
||||||
ParentUUID string `db:"parent_uuid"`
|
ParentUUID string `db:"parent_uuid"`
|
||||||
HistoryUUID string `db:"history_uuid"`
|
|
||||||
Name string
|
Name string
|
||||||
Created time.Time
|
Created time.Time
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
|
|
@ -54,7 +53,11 @@ type Node struct {
|
||||||
DeletedSeq sql.NullInt64 `db:"deleted_seq"`
|
DeletedSeq sql.NullInt64 `db:"deleted_seq"`
|
||||||
Content string
|
Content string
|
||||||
ContentEncrypted string `db:"content_encrypted" json:"-"`
|
ContentEncrypted string `db:"content_encrypted" json:"-"`
|
||||||
Special bool
|
Markdown bool
|
||||||
|
|
||||||
|
// CryptoKeyID int `db:"crypto_key_id"`
|
||||||
|
//Files []File
|
||||||
|
//ChecklistGroups []ChecklistGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func NodeTree(userID, offset int, synced uint64) (nodes []TreeNode, maxSeq uint64, moreRowsExist bool, err error) { // {{{
|
func NodeTree(userID, offset int, synced uint64) (nodes []TreeNode, maxSeq uint64, moreRowsExist bool, err error) { // {{{
|
||||||
|
|
@ -75,7 +78,7 @@ func NodeTree(userID, offset int, synced uint64) (nodes []TreeNode, maxSeq uint6
|
||||||
public.node
|
public.node
|
||||||
WHERE
|
WHERE
|
||||||
user_id = $1 AND
|
user_id = $1 AND
|
||||||
(
|
NOT history AND (
|
||||||
created_seq > $4 OR
|
created_seq > $4 OR
|
||||||
updated_seq > $4 OR
|
updated_seq > $4 OR
|
||||||
deleted_seq > $4
|
deleted_seq > $4
|
||||||
|
|
@ -123,7 +126,7 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node,
|
||||||
rows, err = db.Queryx(`
|
rows, err = db.Queryx(`
|
||||||
SELECT
|
SELECT
|
||||||
uuid,
|
uuid,
|
||||||
COALESCE(parent_uuid, '00000000-0000-0000-0000-000000000000'::uuid) AS parent_uuid,
|
COALESCE(parent_uuid, '') AS parent_uuid,
|
||||||
name,
|
name,
|
||||||
created,
|
created,
|
||||||
updated,
|
updated,
|
||||||
|
|
@ -132,14 +135,14 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node,
|
||||||
updated_seq,
|
updated_seq,
|
||||||
deleted_seq,
|
deleted_seq,
|
||||||
content,
|
content,
|
||||||
content_encrypted
|
content_encrypted,
|
||||||
|
markdown
|
||||||
FROM
|
FROM
|
||||||
public.node
|
public.node
|
||||||
WHERE
|
WHERE
|
||||||
NOT special AND
|
|
||||||
user_id = $1 AND
|
user_id = $1 AND
|
||||||
client != $5::uuid AND
|
client != $5 AND
|
||||||
(
|
NOT history AND (
|
||||||
created_seq > $4 OR
|
created_seq > $4 OR
|
||||||
updated_seq > $4 OR
|
updated_seq > $4 OR
|
||||||
deleted_seq > $4
|
deleted_seq > $4
|
||||||
|
|
@ -192,7 +195,7 @@ func NodesCount(userID int, synced uint64, clientUUID string) (count int, err er
|
||||||
WHERE
|
WHERE
|
||||||
user_id = $1 AND
|
user_id = $1 AND
|
||||||
client != $3 AND
|
client != $3 AND
|
||||||
(
|
NOT history AND (
|
||||||
created_seq > $2 OR
|
created_seq > $2 OR
|
||||||
updated_seq > $2 OR
|
updated_seq > $2 OR
|
||||||
deleted_seq > $2
|
deleted_seq > $2
|
||||||
|
|
@ -245,72 +248,6 @@ func RetrieveNode(userID int, nodeUUID string) (node Node, err error) { // {{{
|
||||||
|
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
func RetrieveNodeHistory(userID int, nodeUUID string, offset int) (nodes []Node, hasMore bool, err error) { // {{{
|
|
||||||
nodes = []Node{}
|
|
||||||
|
|
||||||
var rows *sqlx.Rows
|
|
||||||
rows, err = db.Queryx(`
|
|
||||||
SELECT
|
|
||||||
uuid,
|
|
||||||
history_uuid,
|
|
||||||
user_id,
|
|
||||||
name,
|
|
||||||
created,
|
|
||||||
updated,
|
|
||||||
content,
|
|
||||||
content_encrypted
|
|
||||||
FROM node_history
|
|
||||||
WHERE
|
|
||||||
user_id = $1 AND
|
|
||||||
uuid = $2
|
|
||||||
LIMIT $3 OFFSET $4
|
|
||||||
`,
|
|
||||||
userID,
|
|
||||||
nodeUUID,
|
|
||||||
SYNC_PAGINATION+1,
|
|
||||||
offset,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = werr.Wrap(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
node := Node{}
|
|
||||||
if err = rows.StructScan(&node); err != nil {
|
|
||||||
err = werr.Wrap(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nodes = append(nodes, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nodes) > SYNC_PAGINATION {
|
|
||||||
hasMore = true
|
|
||||||
nodes = nodes[0 : len(nodes)-1]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} // }}}
|
|
||||||
func RetrieveNodeHistoryCount(userID int, nodeUUID string) (count int, err error) { // {{{
|
|
||||||
var row *sql.Row
|
|
||||||
row = db.QueryRow(`
|
|
||||||
SELECT
|
|
||||||
COUNT(*)
|
|
||||||
FROM node_history
|
|
||||||
WHERE
|
|
||||||
user_id = $1 AND
|
|
||||||
uuid = $2
|
|
||||||
`,
|
|
||||||
userID,
|
|
||||||
nodeUUID,
|
|
||||||
)
|
|
||||||
if err = row.Scan(&count); err != nil {
|
|
||||||
err = werr.Wrap(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
} // }}}
|
|
||||||
func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{
|
func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{
|
||||||
var rows *sqlx.Rows
|
var rows *sqlx.Rows
|
||||||
rows, err = db.Queryx(`
|
rows, err = db.Queryx(`
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ $$;
|
||||||
CREATE TABLE public.client (
|
CREATE TABLE public.client (
|
||||||
id integer NOT NULL,
|
id integer NOT NULL,
|
||||||
user_id integer NOT NULL,
|
user_id integer NOT NULL,
|
||||||
client_uuid uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL,
|
client_uuid character(36) DEFAULT ''::bpchar NOT NULL,
|
||||||
created timestamp with time zone DEFAULT now() NOT NULL,
|
created timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
description character varying DEFAULT ''::character varying NOT NULL
|
description character varying DEFAULT ''::character varying NOT NULL
|
||||||
);
|
);
|
||||||
|
|
@ -302,8 +302,8 @@ CREATE SEQUENCE public.node_updates
|
||||||
CREATE TABLE public.node (
|
CREATE TABLE public.node (
|
||||||
id integer NOT NULL,
|
id integer NOT NULL,
|
||||||
user_id integer NOT NULL,
|
user_id integer NOT NULL,
|
||||||
"uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
|
uuid character(36) DEFAULT gen_random_uuid() NOT NULL,
|
||||||
parent_uuid uuid,
|
parent_uuid character(36),
|
||||||
created timestamp with time zone DEFAULT now() NOT NULL,
|
created timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
updated timestamp with time zone DEFAULT now() NOT NULL,
|
updated timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
deleted timestamp with time zone,
|
deleted timestamp with time zone,
|
||||||
|
|
@ -315,7 +315,7 @@ CREATE TABLE public.node (
|
||||||
content_encrypted text DEFAULT ''::text NOT NULL,
|
content_encrypted text DEFAULT ''::text NOT NULL,
|
||||||
markdown boolean DEFAULT false NOT NULL,
|
markdown boolean DEFAULT false NOT NULL,
|
||||||
history boolean DEFAULT false NOT NULL,
|
history boolean DEFAULT false NOT NULL,
|
||||||
client uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL,
|
client character(36) DEFAULT ''::bpchar NOT NULL,
|
||||||
client_sequence integer,
|
client_sequence integer,
|
||||||
CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0))
|
CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0))
|
||||||
);
|
);
|
||||||
|
|
@ -328,7 +328,7 @@ CREATE TABLE public.node (
|
||||||
CREATE TABLE public.node_history (
|
CREATE TABLE public.node_history (
|
||||||
id integer NOT NULL,
|
id integer NOT NULL,
|
||||||
user_id integer NOT NULL,
|
user_id integer NOT NULL,
|
||||||
"uuid" uuid NOT NULL,
|
uuid character(36) NOT NULL,
|
||||||
parents character varying[],
|
parents character varying[],
|
||||||
created timestamp with time zone NOT NULL,
|
created timestamp with time zone NOT NULL,
|
||||||
updated timestamp with time zone NOT NULL,
|
updated timestamp with time zone NOT NULL,
|
||||||
|
|
@ -336,7 +336,7 @@ CREATE TABLE public.node_history (
|
||||||
content text NOT NULL,
|
content text NOT NULL,
|
||||||
content_encrypted text NOT NULL,
|
content_encrypted text NOT NULL,
|
||||||
markdown boolean DEFAULT false NOT NULL,
|
markdown boolean DEFAULT false NOT NULL,
|
||||||
client uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL,
|
client character(36) DEFAULT ''::bpchar NOT NULL,
|
||||||
client_sequence integer
|
client_sequence integer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
168
sql/00002.sql
|
|
@ -1,168 +0,0 @@
|
||||||
CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid character varying, IN p_nodes jsonb)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $procedure$
|
|
||||||
|
|
||||||
DECLARE
|
|
||||||
node_data jsonb;
|
|
||||||
node_updated timestamptz;
|
|
||||||
db_updated timestamptz;
|
|
||||||
db_uuid bpchar;
|
|
||||||
db_client bpchar;
|
|
||||||
db_client_seq int;
|
|
||||||
node_uuid bpchar;
|
|
||||||
parent_uuid bpchar;
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
RAISE NOTICE '--------------------------';
|
|
||||||
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
|
|
||||||
LOOP
|
|
||||||
node_uuid = (node_data->>'UUID')::bpchar;
|
|
||||||
node_updated = (node_data->>'Updated')::timestamptz;
|
|
||||||
|
|
||||||
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN
|
|
||||||
parent_uuid = NULL;
|
|
||||||
ELSE
|
|
||||||
parent_uuid = node_data->>'ParentUUID';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
/* Retrieve the current modified timestamp for this node from the database. */
|
|
||||||
SELECT
|
|
||||||
uuid, updated, client, client_sequence
|
|
||||||
INTO
|
|
||||||
db_uuid, db_updated, db_client, db_client_seq
|
|
||||||
FROM public."node"
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid = node_uuid;
|
|
||||||
|
|
||||||
/* Is the node not in database? It needs to be created. */
|
|
||||||
IF db_uuid IS NULL THEN
|
|
||||||
RAISE NOTICE '01 New node %', node_uuid;
|
|
||||||
INSERT INTO public."node" (
|
|
||||||
user_id, "uuid", parent_uuid, created, updated,
|
|
||||||
"name", "content", markdown, "content_encrypted",
|
|
||||||
client, client_sequence
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id,
|
|
||||||
node_uuid,
|
|
||||||
parent_uuid,
|
|
||||||
(node_data->>'Created')::timestamptz,
|
|
||||||
(node_data->>'Updated')::timestamptz,
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
(node_data->>'Markdown')::bool,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid,
|
|
||||||
(node_data->>'ClientSequence')::int
|
|
||||||
);
|
|
||||||
CONTINUE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
/* The client could send a specific node again if it didn't receive the OK from this procedure before. */
|
|
||||||
IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN
|
|
||||||
RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq;
|
|
||||||
CONTINUE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
/* Determine if the incoming node data is to go into history or replace the current node. */
|
|
||||||
IF db_updated > node_updated THEN
|
|
||||||
RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid;
|
|
||||||
/* Incoming node is going straight to history since it is older than the current node. */
|
|
||||||
INSERT INTO node_history(
|
|
||||||
user_id, "uuid", parents, created, updated,
|
|
||||||
"name", "content", markdown, "content_encrypted",
|
|
||||||
client, client_sequence
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id,
|
|
||||||
node_uuid,
|
|
||||||
(jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
|
|
||||||
(node_data->>'Created')::timestamptz,
|
|
||||||
(node_data->>'Updated')::timestamptz,
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
(node_data->>'Markdown')::bool,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid,
|
|
||||||
(node_data->>'ClientSequence')::int
|
|
||||||
)
|
|
||||||
ON CONFLICT (client, client_sequence)
|
|
||||||
DO NOTHING;
|
|
||||||
ELSE
|
|
||||||
RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence');
|
|
||||||
/* Incoming node is newer and will replace the current node.
|
|
||||||
*
|
|
||||||
* The current node is copied to the node_history table and then modified in place
|
|
||||||
* with the incoming data. */
|
|
||||||
INSERT INTO node_history(
|
|
||||||
user_id, "uuid", parents,
|
|
||||||
created, updated, "name", "content", markdown, "content_encrypted",
|
|
||||||
client, client_sequence
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
user_id,
|
|
||||||
"uuid",
|
|
||||||
(
|
|
||||||
WITH RECURSIVE nodes AS (
|
|
||||||
SELECT
|
|
||||||
uuid,
|
|
||||||
COALESCE(node.parent_uuid, '') AS parent_uuid,
|
|
||||||
name,
|
|
||||||
0 AS depth
|
|
||||||
FROM node
|
|
||||||
WHERE
|
|
||||||
uuid = node_uuid
|
|
||||||
|
|
||||||
UNION
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
n.uuid,
|
|
||||||
COALESCE(n.parent_uuid, '') AS parent_uuid,
|
|
||||||
n.name,
|
|
||||||
nr.depth+1 AS depth
|
|
||||||
FROM node n
|
|
||||||
INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
|
|
||||||
)
|
|
||||||
SELECT ARRAY (
|
|
||||||
SELECT name
|
|
||||||
FROM nodes
|
|
||||||
ORDER BY depth DESC
|
|
||||||
OFFSET 1 /* discard itself */
|
|
||||||
)
|
|
||||||
),
|
|
||||||
created,
|
|
||||||
updated,
|
|
||||||
name,
|
|
||||||
content,
|
|
||||||
markdown,
|
|
||||||
content_encrypted,
|
|
||||||
client,
|
|
||||||
client_sequence
|
|
||||||
FROM public."node"
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid = node_uuid
|
|
||||||
ON CONFLICT (client, client_sequence)
|
|
||||||
DO NOTHING;
|
|
||||||
|
|
||||||
/* Current node in database is updated with incoming data. */
|
|
||||||
UPDATE public."node"
|
|
||||||
SET
|
|
||||||
updated = (node_data->>'Updated')::timestamptz,
|
|
||||||
updated_seq = nextval('node_updates'),
|
|
||||||
name = (node_data->>'Name')::varchar,
|
|
||||||
content = (node_data->>'Content')::text,
|
|
||||||
markdown = (node_data->>'Markdown')::bool,
|
|
||||||
client = p_client_uuid,
|
|
||||||
client_sequence = (node_data->>'ClientSequence')::int
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid = node_uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END LOOP;
|
|
||||||
END
|
|
||||||
$procedure$
|
|
||||||
;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE public.node_history ADD history_uuid uuid NULL;
|
|
||||||
135
sql/00004.sql
|
|
@ -1,135 +0,0 @@
|
||||||
CREATE UNIQUE INDEX node_history_user_id_idx ON public.node_history (user_id,"uuid",history_uuid);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.node ALTER COLUMN "uuid" TYPE uuid USING "uuid"::uuid::uuid;
|
|
||||||
ALTER TABLE public.node ALTER COLUMN parent_uuid TYPE uuid USING parent_uuid::uuid::uuid;
|
|
||||||
ALTER TABLE public.node ALTER COLUMN client TYPE uuid USING client::uuid::uuid;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid uuid, IN p_nodes jsonb)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $procedure$
|
|
||||||
|
|
||||||
DECLARE
|
|
||||||
node_data jsonb;
|
|
||||||
node_updated timestamptz;
|
|
||||||
db_updated timestamptz;
|
|
||||||
db_uuid uuid;
|
|
||||||
db_client uuid;
|
|
||||||
db_client_seq int;
|
|
||||||
db_history_uuid uuid;
|
|
||||||
node_uuid uuid;
|
|
||||||
node_parent_uuid uuid;
|
|
||||||
node_history_uuid uuid;
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
RAISE NOTICE '--------------------------';
|
|
||||||
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
|
|
||||||
LOOP
|
|
||||||
node_uuid = (node_data->>'UUID')::uuid;
|
|
||||||
node_history_uuid = (node_data->>'HistoryUUID')::uuid;
|
|
||||||
node_updated = (node_data->>'Updated')::timestamptz;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Frontend is using an all-zero UUID to define the root node.
|
|
||||||
-- Database is using NULL.
|
|
||||||
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN
|
|
||||||
node_parent_uuid = NULL;
|
|
||||||
ELSE
|
|
||||||
node_parent_uuid = (node_data->>'ParentUUID')::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Every jode has a new history UUID to keep the history entry uniquely identifiable
|
|
||||||
-- across clients. A history entry could potentially be sent again, but should be
|
|
||||||
-- safe to ignore as every change to a node should have a new history UUID.
|
|
||||||
--
|
|
||||||
-- The current node is also stored as history.
|
|
||||||
INSERT INTO node_history(
|
|
||||||
user_id, "uuid", "history_uuid", parents, created, updated,
|
|
||||||
"name", "content", markdown, "content_encrypted",
|
|
||||||
client, client_sequence
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id, -- combined key
|
|
||||||
node_uuid, -- combined key
|
|
||||||
node_history_uuid, -- combined key
|
|
||||||
(jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
|
|
||||||
(node_data->>'Created')::timestamptz,
|
|
||||||
(node_data->>'Updated')::timestamptz,
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
(node_data->>'Markdown')::bool,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid,
|
|
||||||
(node_data->>'ClientSequence')::int
|
|
||||||
)
|
|
||||||
ON CONFLICT ("user_id", "uuid", "history_uuid")
|
|
||||||
DO NOTHING;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Retrieve the current modified timestamp for this node from the database.
|
|
||||||
SELECT
|
|
||||||
uuid, updated, client, client_sequence
|
|
||||||
INTO
|
|
||||||
db_uuid, db_updated, db_client, db_client_seq
|
|
||||||
FROM public."node"
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Is the node not in database? It needs to be created.
|
|
||||||
IF db_uuid IS NULL THEN
|
|
||||||
RAISE NOTICE '01 New node %', node_uuid;
|
|
||||||
|
|
||||||
INSERT INTO public."node" (
|
|
||||||
user_id, "uuid", parent_uuid, created, updated,
|
|
||||||
"name", "content", markdown, "content_encrypted",
|
|
||||||
client, client_sequence
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id,
|
|
||||||
node_uuid,
|
|
||||||
node_parent_uuid,
|
|
||||||
(node_data->>'Created')::timestamptz,
|
|
||||||
(node_data->>'Updated')::timestamptz,
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
(node_data->>'Markdown')::bool,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid,
|
|
||||||
(node_data->>'ClientSequence')::int
|
|
||||||
);
|
|
||||||
|
|
||||||
CONTINUE;
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Update the public node as well if it was older than incoming node.
|
|
||||||
IF node_updated > db_updated THEN
|
|
||||||
UPDATE public."node"
|
|
||||||
SET
|
|
||||||
updated = (node_data->>'Updated')::timestamptz,
|
|
||||||
updated_seq = nextval('node_updates'),
|
|
||||||
name = (node_data->>'Name')::varchar,
|
|
||||||
content = (node_data->>'Content')::text,
|
|
||||||
markdown = (node_data->>'Markdown')::bool,
|
|
||||||
client = p_client_uuid,
|
|
||||||
client_sequence = (node_data->>'ClientSequence')::int
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END LOOP;
|
|
||||||
END
|
|
||||||
$procedure$
|
|
||||||
;
|
|
||||||
129
sql/00005.sql
|
|
@ -1,129 +0,0 @@
|
||||||
-- Some cleanup of old columns not used anymore.
|
|
||||||
DROP INDEX public.node_history_client_idx;
|
|
||||||
ALTER TABLE public.node_history DROP COLUMN client_sequence;
|
|
||||||
|
|
||||||
ALTER TABLE public.node DROP COLUMN markdown;
|
|
||||||
DROP INDEX public.node_history_idx;
|
|
||||||
ALTER TABLE public.node DROP COLUMN history;
|
|
||||||
ALTER TABLE public.node DROP COLUMN client_sequence;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid uuid, IN p_nodes jsonb)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $procedure$
|
|
||||||
|
|
||||||
DECLARE
|
|
||||||
node_data jsonb;
|
|
||||||
node_updated timestamptz;
|
|
||||||
db_updated timestamptz;
|
|
||||||
db_uuid uuid;
|
|
||||||
db_client uuid;
|
|
||||||
db_history_uuid uuid;
|
|
||||||
node_uuid uuid;
|
|
||||||
node_parent_uuid uuid;
|
|
||||||
node_history_uuid uuid;
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
|
|
||||||
LOOP
|
|
||||||
node_uuid = (node_data->>'UUID')::uuid;
|
|
||||||
node_history_uuid = (node_data->>'HistoryUUID')::uuid;
|
|
||||||
node_updated = (node_data->>'Updated')::timestamptz;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Frontend is using an all-zero UUID to define the root node.
|
|
||||||
-- Database is using NULL.
|
|
||||||
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' OR node_data->>'ParentUUID' = '' THEN
|
|
||||||
node_parent_uuid = NULL;
|
|
||||||
ELSE
|
|
||||||
node_parent_uuid = (node_data->>'ParentUUID')::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Every jode has a new history UUID to keep the history entry uniquely identifiable
|
|
||||||
-- across clients. A history entry could potentially be sent again, but should be
|
|
||||||
-- safe to ignore as every change to a node should have a new history UUID.
|
|
||||||
--
|
|
||||||
-- The current node is also stored as history.
|
|
||||||
INSERT INTO node_history(
|
|
||||||
user_id, "uuid", "history_uuid", parents, created, updated,
|
|
||||||
"name", "content", "content_encrypted",
|
|
||||||
client
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id, -- combined key
|
|
||||||
node_uuid, -- combined key
|
|
||||||
node_history_uuid, -- combined key
|
|
||||||
(jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
|
|
||||||
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
|
||||||
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid
|
|
||||||
)
|
|
||||||
ON CONFLICT ("user_id", "uuid", "history_uuid")
|
|
||||||
DO NOTHING;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Retrieve the current modified timestamp for this node from the database.
|
|
||||||
SELECT
|
|
||||||
uuid, updated, client
|
|
||||||
INTO
|
|
||||||
db_uuid, db_updated, db_client
|
|
||||||
FROM public."node"
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Is the node not in database? It needs to be created.
|
|
||||||
IF db_uuid IS NULL THEN
|
|
||||||
RAISE NOTICE '01 New node %', node_uuid;
|
|
||||||
|
|
||||||
INSERT INTO public."node" (
|
|
||||||
user_id, "uuid", parent_uuid, created, updated,
|
|
||||||
"name", "content", "content_encrypted",
|
|
||||||
client
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id,
|
|
||||||
node_uuid,
|
|
||||||
node_parent_uuid,
|
|
||||||
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
|
||||||
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid
|
|
||||||
);
|
|
||||||
|
|
||||||
CONTINUE;
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Update the public node as well if it was older than incoming node.
|
|
||||||
IF node_updated > db_updated THEN
|
|
||||||
UPDATE public."node"
|
|
||||||
SET
|
|
||||||
updated = (node_data->>'Updated')::timestamptz,
|
|
||||||
updated_seq = nextval('node_updates'),
|
|
||||||
name = (node_data->>'Name')::varchar,
|
|
||||||
content = (node_data->>'Content')::text,
|
|
||||||
client = p_client_uuid
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END LOOP;
|
|
||||||
END
|
|
||||||
$procedure$
|
|
||||||
;
|
|
||||||
119
sql/00006.sql
|
|
@ -1,119 +0,0 @@
|
||||||
CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid uuid, IN p_nodes jsonb)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $procedure$
|
|
||||||
|
|
||||||
DECLARE
|
|
||||||
node_data jsonb;
|
|
||||||
node_updated timestamptz;
|
|
||||||
db_updated timestamptz;
|
|
||||||
db_uuid uuid;
|
|
||||||
db_client uuid;
|
|
||||||
db_history_uuid uuid;
|
|
||||||
node_uuid uuid;
|
|
||||||
node_parent_uuid uuid;
|
|
||||||
node_history_uuid uuid;
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
|
|
||||||
LOOP
|
|
||||||
node_uuid = (node_data->>'UUID')::uuid;
|
|
||||||
node_history_uuid = (node_data->>'HistoryUUID')::uuid;
|
|
||||||
node_updated = (node_data->>'Updated')::timestamptz;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Frontend is using an all-zero UUID to define the root node.
|
|
||||||
-- Database is using NULL.
|
|
||||||
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' OR node_data->>'ParentUUID' = '' THEN
|
|
||||||
node_parent_uuid = NULL;
|
|
||||||
ELSE
|
|
||||||
node_parent_uuid = (node_data->>'ParentUUID')::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Every jode has a new history UUID to keep the history entry uniquely identifiable
|
|
||||||
-- across clients. A history entry could potentially be sent again, but should be
|
|
||||||
-- safe to ignore as every change to a node should have a new history UUID.
|
|
||||||
--
|
|
||||||
-- The current node is also stored as history.
|
|
||||||
INSERT INTO node_history(
|
|
||||||
user_id, "uuid", "history_uuid", parents, created, updated,
|
|
||||||
"name", "content", "content_encrypted",
|
|
||||||
client
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id, -- combined key
|
|
||||||
node_uuid, -- combined key
|
|
||||||
node_history_uuid, -- combined key
|
|
||||||
(jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
|
|
||||||
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
|
||||||
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid
|
|
||||||
)
|
|
||||||
ON CONFLICT ("user_id", "uuid", "history_uuid")
|
|
||||||
DO NOTHING;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Retrieve the current modified timestamp for this node from the database.
|
|
||||||
SELECT
|
|
||||||
uuid, updated, client
|
|
||||||
INTO
|
|
||||||
db_uuid, db_updated, db_client
|
|
||||||
FROM public."node"
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Is the node not in database? It needs to be created.
|
|
||||||
IF db_uuid IS NULL THEN
|
|
||||||
RAISE NOTICE '01 New node %', node_uuid;
|
|
||||||
|
|
||||||
INSERT INTO public."node" (
|
|
||||||
user_id, "uuid", parent_uuid, created, updated,
|
|
||||||
"name", "content", "content_encrypted",
|
|
||||||
client
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id,
|
|
||||||
node_uuid,
|
|
||||||
node_parent_uuid,
|
|
||||||
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
|
||||||
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid
|
|
||||||
);
|
|
||||||
|
|
||||||
CONTINUE;
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Update the public node as well if it was older than incoming node.
|
|
||||||
IF node_updated > db_updated THEN
|
|
||||||
UPDATE public."node"
|
|
||||||
SET
|
|
||||||
updated = (node_data->>'Updated')::timestamptz,
|
|
||||||
updated_seq = nextval('node_updates'),
|
|
||||||
parent_uuid = (node_data->>'ParentUUID')::uuid,
|
|
||||||
name = (node_data->>'Name')::varchar,
|
|
||||||
content = (node_data->>'Content')::text,
|
|
||||||
client = p_client_uuid
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END LOOP;
|
|
||||||
END
|
|
||||||
$procedure$
|
|
||||||
;
|
|
||||||
119
sql/00007.sql
|
|
@ -1,119 +0,0 @@
|
||||||
CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid uuid, IN p_nodes jsonb)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $procedure$
|
|
||||||
|
|
||||||
DECLARE
|
|
||||||
node_data jsonb;
|
|
||||||
node_updated timestamptz;
|
|
||||||
db_updated timestamptz;
|
|
||||||
db_uuid uuid;
|
|
||||||
db_client uuid;
|
|
||||||
db_history_uuid uuid;
|
|
||||||
node_uuid uuid;
|
|
||||||
node_parent_uuid uuid;
|
|
||||||
node_history_uuid uuid;
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
|
|
||||||
LOOP
|
|
||||||
node_uuid = (node_data->>'UUID')::uuid;
|
|
||||||
node_history_uuid = (node_data->>'HistoryUUID')::uuid;
|
|
||||||
node_updated = (node_data->>'Updated')::timestamptz;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Frontend is using an all-zero UUID to define the root node.
|
|
||||||
-- Database is using NULL.
|
|
||||||
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' OR node_data->>'ParentUUID' = '' THEN
|
|
||||||
node_parent_uuid = NULL;
|
|
||||||
ELSE
|
|
||||||
node_parent_uuid = (node_data->>'ParentUUID')::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Every jode has a new history UUID to keep the history entry uniquely identifiable
|
|
||||||
-- across clients. A history entry could potentially be sent again, but should be
|
|
||||||
-- safe to ignore as every change to a node should have a new history UUID.
|
|
||||||
--
|
|
||||||
-- The current node is also stored as history.
|
|
||||||
INSERT INTO node_history(
|
|
||||||
user_id, "uuid", "history_uuid", parents, created, updated,
|
|
||||||
"name", "content", "content_encrypted",
|
|
||||||
client
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id, -- combined key
|
|
||||||
node_uuid, -- combined key
|
|
||||||
node_history_uuid, -- combined key
|
|
||||||
(jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
|
|
||||||
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
|
||||||
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid
|
|
||||||
)
|
|
||||||
ON CONFLICT ("user_id", "uuid", "history_uuid")
|
|
||||||
DO NOTHING;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Retrieve the current modified timestamp for this node from the database.
|
|
||||||
SELECT
|
|
||||||
uuid, updated, client
|
|
||||||
INTO
|
|
||||||
db_uuid, db_updated, db_client
|
|
||||||
FROM public."node"
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Is the node not in database? It needs to be created.
|
|
||||||
IF db_uuid IS NULL THEN
|
|
||||||
RAISE NOTICE '01 New node %', node_uuid;
|
|
||||||
|
|
||||||
INSERT INTO public."node" (
|
|
||||||
user_id, "uuid", parent_uuid, created, updated,
|
|
||||||
"name", "content", "content_encrypted",
|
|
||||||
client
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id,
|
|
||||||
node_uuid,
|
|
||||||
node_parent_uuid,
|
|
||||||
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
|
||||||
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid
|
|
||||||
);
|
|
||||||
|
|
||||||
CONTINUE;
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Update the public node as well if it was older than incoming node.
|
|
||||||
IF node_updated > db_updated THEN
|
|
||||||
UPDATE public."node"
|
|
||||||
SET
|
|
||||||
updated = (node_data->>'Updated')::timestamptz,
|
|
||||||
updated_seq = nextval('node_updates'),
|
|
||||||
parent_uuid = node_parent_uuid,
|
|
||||||
name = (node_data->>'Name')::varchar,
|
|
||||||
content = (node_data->>'Content')::text,
|
|
||||||
client = p_client_uuid
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END LOOP;
|
|
||||||
END
|
|
||||||
$procedure$
|
|
||||||
;
|
|
||||||
123
sql/00008.sql
|
|
@ -1,123 +0,0 @@
|
||||||
CREATE OR REPLACE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid uuid, IN p_nodes jsonb)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $procedure$
|
|
||||||
|
|
||||||
DECLARE
|
|
||||||
node_data jsonb;
|
|
||||||
node_updated timestamptz;
|
|
||||||
db_updated timestamptz;
|
|
||||||
db_uuid uuid;
|
|
||||||
db_client uuid;
|
|
||||||
db_history_uuid uuid;
|
|
||||||
node_uuid uuid;
|
|
||||||
node_parent_uuid uuid;
|
|
||||||
node_history_uuid uuid;
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
|
|
||||||
LOOP
|
|
||||||
node_uuid = (node_data->>'UUID')::uuid;
|
|
||||||
node_history_uuid = (node_data->>'HistoryUUID')::uuid;
|
|
||||||
node_updated = (node_data->>'Updated')::timestamptz;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Frontend is using an all-zero UUID to define the root node.
|
|
||||||
-- Database is using NULL.
|
|
||||||
IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' OR node_data->>'ParentUUID' = '' THEN
|
|
||||||
node_parent_uuid = NULL;
|
|
||||||
ELSE
|
|
||||||
node_parent_uuid = (node_data->>'ParentUUID')::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Safeguard against being your own parent.
|
|
||||||
IF node_uuid = node_parent_uuid THEN
|
|
||||||
RAISE EXCEPTION 'Node UUID is same as node parent UUID.' USING ERRCODE = 'XPRNT';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
-- Every jode has a new history UUID to keep the history entry uniquely identifiable
|
|
||||||
-- across clients. A history entry could potentially be sent again, but should be
|
|
||||||
-- safe to ignore as every change to a node should have a new history UUID.
|
|
||||||
--
|
|
||||||
-- The current node is also stored as history.
|
|
||||||
INSERT INTO node_history(
|
|
||||||
user_id, "uuid", "history_uuid", parents, created, updated,
|
|
||||||
"name", "content", "content_encrypted",
|
|
||||||
client
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id, -- combined key
|
|
||||||
node_uuid, -- combined key
|
|
||||||
node_history_uuid, -- combined key
|
|
||||||
(jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
|
|
||||||
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
|
||||||
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid
|
|
||||||
)
|
|
||||||
ON CONFLICT ("user_id", "uuid", "history_uuid")
|
|
||||||
DO NOTHING;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Retrieve the current modified timestamp for this node from the database.
|
|
||||||
SELECT
|
|
||||||
uuid, updated, client
|
|
||||||
INTO
|
|
||||||
db_uuid, db_updated, db_client
|
|
||||||
FROM public."node"
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Is the node not in database? It needs to be created.
|
|
||||||
IF db_uuid IS NULL THEN
|
|
||||||
RAISE NOTICE '01 New node %', node_uuid;
|
|
||||||
|
|
||||||
INSERT INTO public."node" (
|
|
||||||
user_id, "uuid", parent_uuid, created, updated,
|
|
||||||
"name", "content", "content_encrypted",
|
|
||||||
client
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
p_user_id,
|
|
||||||
node_uuid,
|
|
||||||
node_parent_uuid,
|
|
||||||
COALESCE((node_data->>'Created')::timestamptz, NOW()),
|
|
||||||
COALESCE((node_data->>'Updated')::timestamptz, NOW()),
|
|
||||||
(node_data->>'Name')::varchar,
|
|
||||||
(node_data->>'Content')::text,
|
|
||||||
'', /* content_encrypted */
|
|
||||||
p_client_uuid
|
|
||||||
);
|
|
||||||
|
|
||||||
CONTINUE;
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Update the public node as well if it was older than incoming node.
|
|
||||||
IF node_updated > db_updated THEN
|
|
||||||
UPDATE public."node"
|
|
||||||
SET
|
|
||||||
updated = (node_data->>'Updated')::timestamptz,
|
|
||||||
updated_seq = nextval('node_updates'),
|
|
||||||
parent_uuid = node_parent_uuid,
|
|
||||||
name = (node_data->>'Name')::varchar,
|
|
||||||
content = (node_data->>'Content')::text,
|
|
||||||
client = p_client_uuid
|
|
||||||
WHERE
|
|
||||||
user_id = p_user_id AND
|
|
||||||
uuid::uuid = node_uuid::uuid;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END LOOP;
|
|
||||||
END
|
|
||||||
$procedure$
|
|
||||||
;
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
-- Special node such as orphaned and deleted nodes.
|
|
||||||
ALTER TABLE public.node ADD special bool DEFAULT false NOT NULL;
|
|
||||||
|
|
||||||
|
|
||||||
-- Needs to be dropped in order to drop the index on UUID.
|
|
||||||
ALTER TABLE public.node DROP CONSTRAINT node_node_fk;
|
|
||||||
|
|
||||||
-- Index was missing user ID.
|
|
||||||
DROP INDEX public.node_uuid_idx;
|
|
||||||
CREATE UNIQUE INDEX node_user_uuid_idx ON public.node (user_id,"uuid");
|
|
||||||
|
|
||||||
-- Restore the "foreign" key of parent UUID back to UUID.
|
|
||||||
ALTER TABLE public.node ADD CONSTRAINT node_node_fk FOREIGN KEY (user_id,parent_uuid) REFERENCES public.node(user_id,"uuid") ON DELETE RESTRICT ON UPDATE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
-- Auto-create the special nodes for each user.
|
|
||||||
CREATE OR REPLACE FUNCTION create_user_nodes()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
-- NEW holds the row being created.
|
|
||||||
-- No semi-colons omitted here, PL/pgSQL requires them.
|
|
||||||
INSERT INTO public.node (user_id, uuid, parent_uuid, special, name)
|
|
||||||
VALUES
|
|
||||||
(NEW.id, '00000000-0000-0000-0000-000000000000'::uuid, null, true, 'Start'),
|
|
||||||
(NEW.id, '00000000-0000-0000-0000-000000000001'::uuid, null, true, 'Orphaned nodes'),
|
|
||||||
(NEW.id, '00000000-0000-0000-0000-000000000002'::uuid, null, true, 'Deleted nodes');
|
|
||||||
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_after_user_insert
|
|
||||||
AFTER INSERT ON public.user
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION create_user_nodes();
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE public."user" ADD preferences jsonb DEFAULT '{}' NOT NULL;
|
|
||||||
|
|
@ -1,73 +1,34 @@
|
||||||
.el-node-markdown {
|
.el-node-markdown {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
|
|
||||||
.heading-container {
|
h1 {
|
||||||
display: grid;
|
border-bottom: 1px solid #ccc;
|
||||||
grid-template-columns: min-content 1fr;
|
margin-top: 32px;
|
||||||
grid-gap: 12px;
|
margin-bottom: 8px;
|
||||||
white-space: nowrap;
|
|
||||||
align-items: center;
|
display: inline-block;
|
||||||
margin-bottom: 16px;
|
font-size: 1.25em;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--color1);
|
||||||
|
padding: 4px 12px;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 32px !important;
|
margin-top: 32px;
|
||||||
.line {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.line {
|
|
||||||
border-bottom: 1px solid var(--line-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-heading="1"] {
|
|
||||||
margin-top: 64px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-heading="2"],
|
|
||||||
&[data-heading="3"] {
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
.line {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 1.25em;
|
|
||||||
|
|
||||||
clip-path: polygon(0 0, 100% 0, calc(100% - 16px) 100%, 0 100%);
|
|
||||||
|
|
||||||
color: #fff;
|
|
||||||
background-color: var(--color1);
|
|
||||||
padding: 4px 24px 4px 16px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.25em;
|
|
||||||
color: var(--color1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
&:before {
|
|
||||||
font-size: 1.0em;
|
|
||||||
content: "> ";
|
|
||||||
color: var(--color1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
h2 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
margin-top: 32px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
color: var(--color1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3:before {
|
||||||
|
font-size: 1.0em;
|
||||||
|
content: "> ";
|
||||||
color: var(--color1);
|
color: var(--color1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +44,7 @@
|
||||||
table {
|
table {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 16px;
|
margin-top: 14px;
|
||||||
|
|
||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
@ -102,11 +63,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 {
|
||||||
|
|
@ -114,15 +70,6 @@
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -4,170 +4,51 @@
|
||||||
--content-width: 900px;
|
--content-width: 900px;
|
||||||
--thumbnail-width: 300px;
|
--thumbnail-width: 300px;
|
||||||
--thumbnail-height: 100px;
|
--thumbnail-height: 100px;
|
||||||
|
|
||||||
--colorize: invert(59%) sepia(71%) saturate(3270%) hue-rotate(327deg) brightness(100%) contrast(99%);
|
|
||||||
|
|
||||||
--line-color: #ccc;
|
|
||||||
--tree-expander: 0px;
|
|
||||||
--functions-width: 150px;
|
|
||||||
|
|
||||||
--menu-color: #fff;
|
|
||||||
--menu-item-hover-color: #f4f4f4;
|
|
||||||
|
|
||||||
--font-monospace: "Liberation Mono", monospace;
|
|
||||||
|
|
||||||
--markdown-copy-border: 1px solid #0a0;
|
|
||||||
--markdown-copy-background: #e3f4d7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorize {
|
|
||||||
filter: var(--colorize);
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
font-family: var(--font-monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 1em;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------- *
|
|
||||||
* Default application grid in wide mode *
|
|
||||||
* ------------------------------------- */
|
|
||||||
#notes2 {
|
#notes2 {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"tree hum crumbs crumbs ding"
|
||||||
|
"tree hum name name ding"
|
||||||
|
"tree hum sync functions ding"
|
||||||
|
"tree hum content content ding"
|
||||||
|
"tree hum blank blank ding"
|
||||||
|
;
|
||||||
|
grid-template-columns: min-content minmax(16px, 1fr) minmax(min-content, calc(900px - 120px)) 120px minmax(16px, 1fr);
|
||||||
|
grid-template-rows:
|
||||||
|
min-content min-content 48px 1fr;
|
||||||
|
|
||||||
&.page-node {
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"tree-expander tree pad1 crumbs crumbs pad2"
|
"crumbs"
|
||||||
"tree-expander tree pad1 name functions pad2"
|
"sync"
|
||||||
"tree-expander tree pad1 content content pad2"
|
"name"
|
||||||
|
"content"
|
||||||
|
"blank"
|
||||||
;
|
;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
grid-template-columns:
|
|
||||||
/* Tree-expander */
|
|
||||||
var(--tree-expander)
|
|
||||||
/* Tree */
|
|
||||||
min-content minmax(32px, 1fr)
|
|
||||||
/* Sync */
|
|
||||||
minmax(min-content, calc(var(--content-width) - var(--functions-width)))
|
|
||||||
/* Functions */
|
|
||||||
var(--functions-width)
|
|
||||||
/* Content */
|
|
||||||
minmax(32px, 1fr);
|
|
||||||
|
|
||||||
grid-template-rows:
|
|
||||||
/* Crumbs */
|
|
||||||
min-content
|
|
||||||
/* Name */
|
|
||||||
min-content
|
|
||||||
/* Content */
|
|
||||||
1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The other pages just gets the whole page without dividing it up. */
|
|
||||||
&:not(.page-node) {
|
|
||||||
grid-template-areas:
|
|
||||||
"tree-expander tree pad1 n2-page pad2"
|
|
||||||
;
|
|
||||||
|
|
||||||
grid-template-columns:
|
|
||||||
/* Tree-expander */
|
|
||||||
var(--tree-expander)
|
|
||||||
/* Tree */
|
|
||||||
min-content
|
|
||||||
/* pad1 */
|
|
||||||
32px
|
|
||||||
/* Content */
|
|
||||||
1fr
|
|
||||||
/* pad2 */
|
|
||||||
32px;
|
|
||||||
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Tree expander is collapsed as default */
|
|
||||||
--tree-expander: 0px;
|
|
||||||
|
|
||||||
&.hide-tree {
|
|
||||||
--tree-expander: 32px;
|
|
||||||
|
|
||||||
#tree {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
n2-sidebar {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------- *
|
|
||||||
* Application grid in narrow mode *
|
|
||||||
* ------------------------------- */
|
|
||||||
@media only screen and (max-width: 800px) {
|
|
||||||
#notes2 {
|
|
||||||
grid-template-areas:
|
|
||||||
"tree-expander pad1 crumbs crumbs pad2"
|
|
||||||
"tree-expander pad1 name functions pad2"
|
|
||||||
"tree-expander pad1 content content pad2"
|
|
||||||
;
|
|
||||||
grid-template-columns: 32px 16px 1fr var(--functions-width) 16px;
|
|
||||||
|
|
||||||
&.show-tree {
|
|
||||||
grid-template-areas: "tree";
|
|
||||||
grid-template-columns: 100%;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
|
|
||||||
#tree {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main-page,
|
|
||||||
#show-tree {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#tree {
|
#tree {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n2-syncprogress {
|
||||||
|
.el-count {
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#tree-expander {
|
|
||||||
grid-area: tree-expander;
|
|
||||||
color: #333;
|
|
||||||
background-color: #eee;
|
|
||||||
font-weight: bold;
|
|
||||||
border-right: 1px solid var(--line-color);
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
align-items: start;
|
|
||||||
|
|
||||||
padding-top: 8px;
|
|
||||||
font-size: 1.25em;
|
|
||||||
|
|
||||||
div div {
|
|
||||||
display: inline-block;
|
|
||||||
writing-mode: vertical-rl;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#tree {
|
#tree {
|
||||||
grid-area: tree;
|
grid-area: tree;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -175,14 +56,47 @@ button {
|
||||||
color: #444;
|
color: #444;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
border-right: 1px solid var(--line-color);
|
border-right: 2px solid #ddd;
|
||||||
|
|
||||||
n2-sidebar {
|
#logo {
|
||||||
.el-treenodes {
|
display: grid;
|
||||||
margin: 24px 32px 32px 32px;
|
grid-template-columns: min-content 1fr min-content;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: start;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
|
||||||
|
.el-search {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
img:first-child {
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 16px 0px 32px 0px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
n2-tree {
|
||||||
|
.el-treenodes {
|
||||||
|
margin: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
n2-tree {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.node {
|
.node {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 40px min-content;
|
grid-template-columns: 40px min-content;
|
||||||
|
|
@ -199,11 +113,6 @@ button {
|
||||||
img {
|
img {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|
||||||
&.deleted {
|
|
||||||
height: 24px;
|
|
||||||
transform: translateX(3px) translateY(3px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,87 +145,56 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =============== *
|
|
||||||
* PAGE MANAGEMENT *
|
|
||||||
* =============== */
|
|
||||||
[id^="page-"] {
|
[id^="page-"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notes2 {
|
|
||||||
&.page-node {
|
|
||||||
#page-root {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-node {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.page-storage {
|
|
||||||
#page-storage {
|
|
||||||
display: contents;
|
|
||||||
|
|
||||||
n2-pagestorage {
|
|
||||||
grid-area: n2-page;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.page-history {
|
|
||||||
#page-history {
|
|
||||||
display: grid;
|
|
||||||
grid-area: n2-page;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.page-preferences {
|
|
||||||
#page-preferences {
|
|
||||||
display: block;
|
|
||||||
grid-area: n2-page;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.root-node-override {
|
|
||||||
[id^="page-"] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-root {
|
|
||||||
display: contents !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#main-page {
|
#main-page {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
||||||
&:focus-within {
|
&.node {
|
||||||
background-color: #faf;
|
#page-node {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.storage {
|
||||||
|
#page-storage {
|
||||||
|
display: contents;
|
||||||
|
n2-pagestorage {
|
||||||
|
grid-area: content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#crumbs {
|
#crumbs {
|
||||||
grid-area: crumbs;
|
grid-area: crumbs;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
justify-items: start;
|
justify-items: center;
|
||||||
height: min-content;
|
height: min-content;
|
||||||
margin: 0 16px 16px 0px;
|
margin: 0 16px 16px 16px;
|
||||||
|
|
||||||
n2-crumbs {
|
n2-crumbs {
|
||||||
|
background: #e4e4e4;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
padding: 8px 16px;
|
||||||
padding: 16px 0px;
|
background: #e4e4e4;
|
||||||
color: #333;
|
color: #333;
|
||||||
border-bottom-left-radius: 5px;
|
border-bottom-left-radius: 5px;
|
||||||
border-bottom-right-radius: 5px;
|
border-bottom-right-radius: 5px;
|
||||||
|
|
||||||
|
&.node-modified {
|
||||||
|
background-color: var(--color1);
|
||||||
|
color: var(--color2);
|
||||||
|
|
||||||
|
.crumb:after {
|
||||||
|
color: var(--color2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
n2-crumb {
|
n2-crumb {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -329,89 +207,85 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n2-crumb:after {
|
||||||
n2-crumb:before {
|
|
||||||
content: ">";
|
content: ">";
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--color1)
|
color: var(--color1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n2-crumb:last-child {
|
||||||
n2-crumb.home {
|
margin-right: 0;
|
||||||
&:before {
|
|
||||||
content: '';
|
|
||||||
margin-left: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n2-crumb:last-child:after {
|
||||||
|
content: '';
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
n2-syncprogress {
|
n2-syncprogress {
|
||||||
|
--radius: 8px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
position: fixed;
|
grid-area: sync;
|
||||||
top: 8px;
|
display: grid;
|
||||||
right: 8px;
|
justify-items: center;
|
||||||
padding: 8px 16px;
|
align-items: center;
|
||||||
z-index: 16384;
|
|
||||||
border-radius: 6px;
|
position: relative;
|
||||||
font-weight: bold;
|
|
||||||
background-color: var(--color1);
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px;
|
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 250ms;
|
transition: height 0s 500ms, opacity 500ms linear, visibility 0s 500ms;
|
||||||
|
|
||||||
&.show {
|
&.show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transition: visibility, height 0s, opacity 500ms linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ok {
|
progress {
|
||||||
background-color: #5aa02c;
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
grid-template-columns: min-content repeat(3, min-content);
|
.count {
|
||||||
grid-gap: 8px 8px;
|
position: absolute;
|
||||||
white-space: nowrap;
|
top: 16px;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
justify-items: end;
|
white-space: nowrap;
|
||||||
|
color: #888;
|
||||||
img {
|
text-align: center;
|
||||||
grid-row: 1/3;
|
font-size: 12pt;
|
||||||
height: 34px;
|
font-weight: bold;
|
||||||
margin-right: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#page-root {
|
progress[value]::-webkit-progress-bar {
|
||||||
&>div {
|
background-color: #eee;
|
||||||
grid-area: content;
|
box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
|
||||||
align-self: start;
|
border-radius: var(--radius);
|
||||||
margin-top: 64px;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
|
|
||||||
/* logo */
|
|
||||||
img {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create {
|
|
||||||
border: 2px solid #529b00;
|
|
||||||
padding: 16px 32px;
|
|
||||||
margin-top: 64px;
|
|
||||||
background-color: #d9ffc9;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progress[value]::-moz-progress-bar {
|
||||||
|
background-color: #eee;
|
||||||
|
box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress[value]::-webkit-progress-value {
|
||||||
|
background: rgb(186, 95, 89);
|
||||||
|
background: linear-gradient(180deg, rgba(186, 95, 89, 1) 0%, rgba(254, 95, 85, 1) 50%, rgba(186, 95, 89, 1) 100%);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress[value]::-moz-progress-value {
|
||||||
|
background: rgb(186, 95, 89);
|
||||||
|
background: linear-gradient(180deg, rgba(186, 95, 89, 1) 0%, rgba(254, 95, 85, 1) 50%, rgba(186, 95, 89, 1) 100%);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================= */
|
/* ============================================================= */
|
||||||
|
|
@ -419,42 +293,25 @@ n2-syncprogress {
|
||||||
n2-nodeui {
|
n2-nodeui {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
|
||||||
&.node-modified:before {
|
|
||||||
content: 'h';
|
|
||||||
z-index: 8192;
|
|
||||||
position: fixed;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
height: 4px;
|
|
||||||
|
|
||||||
background-color: var(--color1);
|
|
||||||
color: var(--color2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-name {
|
.el-name {
|
||||||
grid-area: name;
|
grid-area: name;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.75em;
|
text-align: center;
|
||||||
|
font-size: 1.15em;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
white-space: nowrap;
|
|
||||||
width: min-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-functions {
|
.el-functions {
|
||||||
grid-area: functions;
|
grid-area: functions;
|
||||||
justify-self: end;
|
|
||||||
align-self: end;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-node-content {
|
.el-node-content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
font-size: 1em;
|
font-family: monospace;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -468,24 +325,21 @@ n2-nodeui {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-top: 1px solid #e0e0e0;
|
border-top: 1px solid #e0e0e0;
|
||||||
border-bottom: none;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
|
||||||
|
&:invalid {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-node-markdown {
|
.el-node-markdown {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
font-family: var(--font-monospace);
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
border-top: 1px solid #e0e0e0;
|
border-top: 1px solid #e0e0e0;
|
||||||
margin-top: 8px;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.show-markdown {
|
&.show-markdown {
|
||||||
|
|
@ -546,46 +400,3 @@ dialog.op {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------- *
|
|
||||||
* Whole page is 100vh with scrolling sections *
|
|
||||||
* ------------------------------------------- */
|
|
||||||
#app.full-height {
|
|
||||||
#notes2 {
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#tree {
|
|
||||||
n2-sidebar {
|
|
||||||
.el-treenodes {
|
|
||||||
height: calc(100vh - 64px - 64px);
|
|
||||||
margin: 0px;
|
|
||||||
padding: 12px 32px 32px 32px;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n2-nodeui {
|
|
||||||
.el-node-markdown {
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
#page-history {
|
|
||||||
container-type: inline-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* View when two columns doesn't fit on screen. */
|
|
||||||
@container (width < 1100px) {
|
|
||||||
n2-pagehistory {
|
|
||||||
grid-template-columns: 1fr minmax(300px, 900px) 1fr !important;
|
|
||||||
|
|
||||||
.column-2 {
|
|
||||||
grid-column: 2 / 3 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* View when not even one column with well on screen */
|
|
||||||
/* Node name is placed on a separate row. */
|
|
||||||
@container (width < 500px) {
|
|
||||||
.el-nodes {
|
|
||||||
grid-template-columns: min-content minmax(min-content, max-content) 1fr !important;
|
|
||||||
background-color: unset !important;
|
|
||||||
border: unset !important;
|
|
||||||
gap: unset !important;
|
|
||||||
|
|
||||||
.el-index {
|
|
||||||
border-top-left-radius: 6px;
|
|
||||||
border-left: 1px solid var(--line-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-index, .el-updated, .el-size {
|
|
||||||
border-top: 1px solid var(--line-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-size {
|
|
||||||
text-align: right;
|
|
||||||
border-right: 1px solid var(--line-color);
|
|
||||||
border-top-right-radius: 6px;
|
|
||||||
padding-right: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-name {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
padding-top: 0px;
|
|
||||||
border-bottom: 1px solid var(--line-color);
|
|
||||||
border-left: 1px solid var(--line-color);
|
|
||||||
border-right: 1px solid var(--line-color);
|
|
||||||
border-bottom-left-radius: 6px;
|
|
||||||
border-bottom-right-radius: 6px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
n2-pagehistorynode > * {
|
|
||||||
padding-left: 8px !important;
|
|
||||||
padding-right: 0px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n2-pagehistory {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: min-content min-content min-content;
|
|
||||||
grid-template-columns: 1fr minmax(600px, 800px) minmax(400px, 900px) 1fr;
|
|
||||||
grid-gap: 0px 32px;
|
|
||||||
|
|
||||||
.column-1 {
|
|
||||||
grid-column: 2 / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-2 {
|
|
||||||
grid-column: 3 / 4;
|
|
||||||
max-width: 900px;
|
|
||||||
|
|
||||||
.group {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.back,
|
|
||||||
.node-name {
|
|
||||||
grid-column: 2 / 4;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: min-content 1fr;
|
|
||||||
grid-gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-label {
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: #444;
|
|
||||||
color: #fff;
|
|
||||||
padding: 8px 32px;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 32px;
|
|
||||||
transform: translateY(14px);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 32px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #fafafa;
|
|
||||||
|
|
||||||
box-shadow:
|
|
||||||
rgba(0, 0, 0, 0.4) 0px 2px 4px,
|
|
||||||
rgba(0, 0, 0, 0.3) 0px 7px 13px -3px,
|
|
||||||
rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-stats {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: min-content 1fr;
|
|
||||||
grid-gap: 8px 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-fetch-history-progress {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-back-image,
|
|
||||||
.el-back-text {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-node-name {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-nodes {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: min-content minmax(min-content, max-content) min-content 1fr;
|
|
||||||
|
|
||||||
background-color: var(--line-color);
|
|
||||||
gap: 1px;
|
|
||||||
border: 1px solid var(--line-color);
|
|
||||||
|
|
||||||
n2-pagehistorynode>* {
|
|
||||||
padding: 8px 12px;
|
|
||||||
background-color: #fff;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
n2-pagehistorynode {
|
|
||||||
|
|
||||||
&.selected .el-index:after {
|
|
||||||
position: absolute;
|
|
||||||
left: -20px;
|
|
||||||
|
|
||||||
content: '>';
|
|
||||||
color: var(--color1);
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-index {
|
|
||||||
position: relative;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-updated {
|
|
||||||
white-space: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-date {
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-time {
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-name {
|
|
||||||
white-space: initial;
|
|
||||||
/*overflow-wrap: anywhere;*/
|
|
||||||
word-break: break-all;
|
|
||||||
color: var(--color1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-pagination {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, min-content);
|
|
||||||
grid-gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
.el-prev,
|
|
||||||
.el-next {
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
background-color: #eee;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="23.999962"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 6.3499898 6.35"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
|
||||||
sodipodi:docname="icon_back.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="0.81067621"
|
|
||||||
inkscape:cx="11.718612"
|
|
||||||
inkscape:cy="12.335381"
|
|
||||||
inkscape:window-width="1916"
|
|
||||||
inkscape:window-height="1161"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1" />
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-101.82501,-145.32499)">
|
|
||||||
<title
|
|
||||||
id="title1">arrow-left-circle</title>
|
|
||||||
<path
|
|
||||||
d="m 101.82501,148.49999 a 3.1749998,3.1749998 0 0 1 3.175,-3.175 3.1749998,3.1749998 0 0 1 3.17499,3.175 3.1749998,3.1749998 0 0 1 -3.17499,3.175 3.1749998,3.1749998 0 0 1 -3.175,-3.175 m 5.08,-0.3175 H 104.365 l 1.11125,-1.11125 -0.45084,-0.45085 -1.87961,1.8796 1.87961,1.8796 0.45084,-0.45085 -1.11125,-1.11125 h 2.54001 z"
|
|
||||||
id="path1"
|
|
||||||
style="stroke-width:0.3175" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,71 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 6.35 6.35"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="icon_drag.svg"
|
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="22.627417"
|
|
||||||
inkscape:cx="11.291611"
|
|
||||||
inkscape:cy="10.84967"
|
|
||||||
inkscape:window-width="2190"
|
|
||||||
inkscape:window-height="1401"
|
|
||||||
inkscape:window-x="1463"
|
|
||||||
inkscape:window-y="18"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false" /><defs
|
|
||||||
id="defs1" /><g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-107.95,-148.16667)"><title
|
|
||||||
id="title1">folder-open</title><title
|
|
||||||
id="title1-1">folder-open-outline</title><title
|
|
||||||
id="title1-5">notebook-outline</title><title
|
|
||||||
id="title1-8">text-box-outline</title><path
|
|
||||||
style="fill:#ffffff;stroke-width:0.264583"
|
|
||||||
d="m 108.3015,148.56838 h 3.95851 v 3.96688 h -3.95851 z"
|
|
||||||
id="path3"
|
|
||||||
sodipodi:nodetypes="ccccc" /><path
|
|
||||||
d="m 108.47917,148.16667 c -0.29369,0 -0.52917,0.23548 -0.52917,0.52917 V 152.4 c 0,0.29369 0.23548,0.52917 0.52917,0.52917 h 3.70416 c 0.29369,0 0.52917,-0.23548 0.52917,-0.52917 v -3.70416 c 0,-0.29369 -0.23548,-0.52917 -0.52917,-0.52917 h -3.70416 m 0,0.52917 h 3.70416 V 152.4 h -3.70416 v -3.70416"
|
|
||||||
id="path1"
|
|
||||||
style="fill:#666666;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
|
||||||
sodipodi:nodetypes="cssssssscccccc" /><path
|
|
||||||
d="m 109.00833,149.225 v 0.52917 h 2.64584 V 149.225 h -2.64584 m 0,1.05834 v 0.52916 h 2.64584 v -0.52916 h -2.64584 m 0,1.05833 v 0.52917 h 2.64584 v -0.52917 z"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#999999;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
sodipodi:nodetypes="ccccccccccccccc" /><g
|
|
||||||
id="g5"
|
|
||||||
transform="translate(0.26458031,0.26458956)"><g
|
|
||||||
id="g8"
|
|
||||||
transform="matrix(1.2067669,0,0,1.2067669,-23.043599,-31.373186)"><circle
|
|
||||||
style="fill:#800000;fill-opacity:1;stroke:none;stroke-width:1.14487;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="path8"
|
|
||||||
cx="112.05721"
|
|
||||||
cy="152.28557"
|
|
||||||
r="1.5347482" /></g><path
|
|
||||||
style="fill:none;stroke:#ffffff;stroke-width:0.79375;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m 111.32748,151.54414 1.71172,1.71172"
|
|
||||||
id="path4" /><path
|
|
||||||
style="fill:none;stroke:#ffffff;stroke-width:0.79375;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m 113.0392,151.54414 -1.71172,1.71172"
|
|
||||||
id="path5" /></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
|
@ -1,75 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 6.35 6.35"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="icon_drag_ok.svg"
|
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="32"
|
|
||||||
inkscape:cx="13.859375"
|
|
||||||
inkscape:cy="14.890625"
|
|
||||||
inkscape:window-width="2190"
|
|
||||||
inkscape:window-height="1401"
|
|
||||||
inkscape:window-x="1463"
|
|
||||||
inkscape:window-y="18"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false" /><defs
|
|
||||||
id="defs1" /><g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-107.95,-148.16667)"><title
|
|
||||||
id="title1">folder-open</title><title
|
|
||||||
id="title1-1">folder-open-outline</title><title
|
|
||||||
id="title1-5">notebook-outline</title><title
|
|
||||||
id="title1-8">text-box-outline</title><path
|
|
||||||
style="fill:#ffffff;stroke-width:0.264583"
|
|
||||||
d="m 108.3015,148.56838 h 3.95851 v 3.96688 h -3.95851 z"
|
|
||||||
id="path3"
|
|
||||||
sodipodi:nodetypes="ccccc" /><title
|
|
||||||
id="title1-53">text-box-check-outline</title><path
|
|
||||||
style="stroke-width:0.264583;fill:#999999;fill-opacity:1"
|
|
||||||
d="m 111.65417,149.75417 h -2.64584 V 149.225 h 2.64584"
|
|
||||||
id="path7" /><path
|
|
||||||
style="fill:#999999;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
d="m 109.00833,150.8125 v -0.52916 h 2.64584 v 0.52916"
|
|
||||||
id="path6"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="fill:#999999;fill-opacity:1;stroke-width:0.313059"
|
|
||||||
d="m 111.65417,151.87084 h -2.64584 v -0.52917 h 2.64584"
|
|
||||||
id="path5"
|
|
||||||
sodipodi:nodetypes="cccc" /><path
|
|
||||||
style="fill:#666666;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
d="m 111.26226,152.92917 h -2.78309 c -0.29369,0 -0.52917,-0.23548 -0.52917,-0.52917 v -3.70416 c 0,-0.29369 0.23548,-0.52917 0.52917,-0.52917 h 3.70416 c 0.29369,0 0.52917,0.23548 0.52917,0.52917 v 2.7004 c -0.1614,-0.0926 -0.33867,-0.15875 -0.52917,-0.1905 v -2.5099 h -3.70416 V 152.4 h 2.59259 c 0.0318,0.1905 0.0979,0.36777 0.1905,0.52917"
|
|
||||||
id="path4"
|
|
||||||
sodipodi:nodetypes="cssssssccccccc" /><g
|
|
||||||
id="g8"
|
|
||||||
transform="matrix(1.2067669,0,0,1.2067669,-23.043599,-31.373186)"><ellipse
|
|
||||||
style="fill:#338000;fill-opacity:1;stroke:none;stroke-width:1.14488;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="path8"
|
|
||||||
cx="112.27646"
|
|
||||||
cy="152.50487"
|
|
||||||
rx="1.5347482"
|
|
||||||
ry="1.5347837" /><path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
d="m 112.01188,153.31978 -0.72761,-0.79375 0.30692,-0.30692 0.42069,0.42069 0.94985,-0.94985 0.30692,0.37306"
|
|
||||||
id="path1-5" /></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
|
@ -1,49 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="15.999988"
|
|
||||||
height="15.999988"
|
|
||||||
viewBox="0 0 4.2333301 4.2333301"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="icon_drag_source.svg"
|
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="8.4025252"
|
|
||||||
inkscape:cx="45.522029"
|
|
||||||
inkscape:cy="-1.1306125"
|
|
||||||
inkscape:window-width="2190"
|
|
||||||
inkscape:window-height="1401"
|
|
||||||
inkscape:window-x="1463"
|
|
||||||
inkscape:window-y="18"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1" />
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-187.14773,-188.73523)">
|
|
||||||
<title
|
|
||||||
id="title1">drag-variant</title>
|
|
||||||
<path
|
|
||||||
d="m 191.38106,190.85189 -0.8907,0.89269 -0.49793,-0.49594 0.39279,-0.39675 -0.39279,-0.38881 0.49793,-0.49792 0.8907,0.88673 m -2.11667,-2.11666 0.88674,0.89071 -0.49791,0.49792 -0.38883,-0.39278 -0.39674,0.39278 -0.49594,-0.49792 0.89268,-0.89071 m 0,4.23333 -0.88673,-0.8907 0.49792,-0.49793 0.38881,0.39279 0.39675,-0.39279 0.49594,0.49793 -0.89269,0.8907 m -2.11666,-2.11667 0.89071,-0.89268 0.49792,0.49594 -0.39278,0.39674 0.39278,0.38883 -0.49792,0.49791 -0.89071,-0.88674 m 2.11666,-0.39674 a 0.3967509,0.3967509 0 0 1 0.39675,0.39674 0.3967509,0.3967509 0 0 1 -0.39675,0.39675 0.3967509,0.3967509 0 0 1 -0.39674,-0.39675 0.3967509,0.3967509 0 0 1 0.39674,-0.39674 z"
|
|
||||||
id="path1"
|
|
||||||
style="stroke-width:0.198375" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -1,49 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="7.40833mm"
|
|
||||||
height="6.3499999mm"
|
|
||||||
viewBox="0 0 7.40833 6.3499999"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
|
||||||
sodipodi:docname="icon_history.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:zoom="1"
|
|
||||||
inkscape:cx="10"
|
|
||||||
inkscape:cy="9"
|
|
||||||
inkscape:window-width="1916"
|
|
||||||
inkscape:window-height="1161"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1" />
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-102.39375,-146.05)">
|
|
||||||
<title
|
|
||||||
id="title1">history</title>
|
|
||||||
<path
|
|
||||||
d="m 106.80347,147.81389 h -0.52916 v 1.76388 l 1.50988,0.89606 0.254,-0.42686 -1.23472,-0.73377 v -1.49931 M 106.62708,146.05 a 3.175,3.175 0 0 0 -3.175,3.175 h -1.05833 l 1.397,1.42169 1.42523,-1.42169 h -1.05834 a 2.4694444,2.4694444 0 0 1 2.46944,-2.46944 2.4694444,2.4694444 0 0 1 2.46944,2.46944 2.4694444,2.4694444 0 0 1 -2.46944,2.46944 c -0.68086,0 -1.29822,-0.27869 -1.74272,-0.72672 l -0.50094,0.50095 c 0.57502,0.57856 1.36172,0.93133 2.24366,0.93133 a 3.175,3.175 0 0 0 3.175,-3.175 3.175,3.175 0 0 0 -3.175,-3.175"
|
|
||||||
id="path1"
|
|
||||||
style="stroke-width:0.352777" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,71 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="20.000013"
|
|
||||||
viewBox="0 0 6.3499998 5.2916702"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
|
||||||
sodipodi:docname="icon_home.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="34.879392"
|
|
||||||
inkscape:cx="11.797797"
|
|
||||||
inkscape:cy="8.3717056"
|
|
||||||
inkscape:window-width="1916"
|
|
||||||
inkscape:window-height="1161"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="true">
|
|
||||||
<inkscape:grid
|
|
||||||
id="grid1"
|
|
||||||
units="px"
|
|
||||||
originx="0"
|
|
||||||
originy="0"
|
|
||||||
spacingx="0.26458333"
|
|
||||||
spacingy="0.26458333"
|
|
||||||
empcolor="#0099e5"
|
|
||||||
empopacity="0.30196078"
|
|
||||||
color="#0099e5"
|
|
||||||
opacity="0.14901961"
|
|
||||||
empspacing="5"
|
|
||||||
enabled="true"
|
|
||||||
visible="true" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-102.39375,-146.31458)">
|
|
||||||
<title
|
|
||||||
id="title1">home-outline</title>
|
|
||||||
<path
|
|
||||||
id="path1"
|
|
||||||
style="stroke-width:0.264583;stroke-dasharray:none;fill:#666666"
|
|
||||||
d="m 105.56875,146.31458 -3.175,2.64583 0.79375,0 v 2.64584 l 2.11667,0 v -1.5875 h 0.26458 v -0.52917 H 104.775 v 1.5875 l -1.05833,0 0,-2.64583 1.85208,-1.5875 z"
|
|
||||||
sodipodi:nodetypes="cccccccccccccc" />
|
|
||||||
<path
|
|
||||||
id="path2"
|
|
||||||
style="stroke-width:0.264583;stroke-dasharray:none;fill:#666666"
|
|
||||||
d="m 105.56875,146.31458 3.175,2.64583 -0.79375,0 v 2.64584 l -2.11667,0 v -1.5875 h -0.26458 v -0.52917 h 0.79375 v 1.5875 l 1.05833,0 0,-2.64583 -1.85208,-1.5875 z"
|
|
||||||
sodipodi:nodetypes="cccccccccccccc" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
|
@ -9,8 +9,8 @@
|
||||||
role="img"
|
role="img"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
sodipodi:docname="icon_markdown.svg"
|
sodipodi:docname="markdown.svg"
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
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"
|
||||||
|
|
@ -33,10 +33,10 @@
|
||||||
inkscape:zoom="0.70710678"
|
inkscape:zoom="0.70710678"
|
||||||
inkscape:cx="453.96255"
|
inkscape:cx="453.96255"
|
||||||
inkscape:cy="60.811183"
|
inkscape:cy="60.811183"
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="2190"
|
||||||
inkscape:window-height="1161"
|
inkscape:window-height="1401"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="1463"
|
||||||
inkscape:window-y="18"
|
inkscape:window-y="0"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="svg1" />
|
inkscape:current-layer="svg1" />
|
||||||
<title
|
<title
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
<path
|
<path
|
||||||
d="M 338.73829,224.67957 H 26.316564 A 26.316564,26.316564 0 0 1 0,198.363 V 26.316564 A 26.316564,26.316564 0 0 1 26.316564,0 H 338.73829 a 26.316564,26.316564 0 0 1 26.31657,26.316564 V 198.33258 a 26.316564,26.316564 0 0 1 -26.31657,26.33177 z M 87.742162,172.01601 v -68.45349 l 35.109038,43.8863 35.09382,-43.8863 v 68.45349 h 35.10903 V 52.678763 H 157.94502 L 122.8512,96.565057 87.742162,52.678763 H 52.633128 V 172.04644 Z M 322.94835,112.33978 H 287.83932 V 52.663552 H 252.7455 v 59.676228 h -35.10904 l 52.64834,61.44081 z"
|
d="M 338.73829,224.67957 H 26.316564 A 26.316564,26.316564 0 0 1 0,198.363 V 26.316564 A 26.316564,26.316564 0 0 1 26.316564,0 H 338.73829 a 26.316564,26.316564 0 0 1 26.31657,26.316564 V 198.33258 a 26.316564,26.316564 0 0 1 -26.31657,26.33177 z M 87.742162,172.01601 v -68.45349 l 35.109038,43.8863 35.09382,-43.8863 v 68.45349 h 35.10903 V 52.678763 H 157.94502 L 122.8512,96.565057 87.742162,52.678763 H 52.633128 V 172.04644 Z M 322.94835,112.33978 H 287.83932 V 52.663552 H 252.7455 v 59.676228 h -35.10904 l 52.64834,61.44081 z"
|
||||||
id="path1"
|
id="path1"
|
||||||
style="stroke-width:15.2119;fill:#000000;fill-opacity:1" />
|
style="stroke-width:15.2119;fill:#fe5f55;fill-opacity:1" />
|
||||||
<metadata
|
<metadata
|
||||||
id="metadata1">
|
id="metadata1">
|
||||||
<rdf:RDF>
|
<rdf:RDF>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
|
@ -1,56 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="12"
|
|
||||||
height="23.999981"
|
|
||||||
viewBox="0 0 3.1750001 6.349995"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
|
||||||
sodipodi:docname="icon_menu.svg"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="11.859035"
|
|
||||||
inkscape:cx="8.6010372"
|
|
||||||
inkscape:cy="17.32856"
|
|
||||||
inkscape:window-width="2190"
|
|
||||||
inkscape:window-height="1401"
|
|
||||||
inkscape:window-x="1463"
|
|
||||||
inkscape:window-y="18"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1" /><defs
|
|
||||||
id="defs1" /><g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-147.15925,-92.339586)"><title
|
|
||||||
id="title1">menu</title><title
|
|
||||||
id="title1-6">hamburger</title><circle
|
|
||||||
style="fill:#000000;stroke:none;stroke-width:0.264583"
|
|
||||||
id="path3"
|
|
||||||
cx="149.55338"
|
|
||||||
cy="93.120461"
|
|
||||||
r="0.78087437" /><circle
|
|
||||||
style="fill:#000000;stroke:none;stroke-width:0.264583"
|
|
||||||
id="circle4"
|
|
||||||
cx="149.55338"
|
|
||||||
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 |
|
|
@ -1,49 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="21.714331"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 5.7452499 6.35"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
|
||||||
sodipodi:docname="icon_new_document.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="0.74118967"
|
|
||||||
inkscape:cx="8.769685"
|
|
||||||
inkscape:cy="10.793459"
|
|
||||||
inkscape:window-width="1916"
|
|
||||||
inkscape:window-height="1041"
|
|
||||||
inkscape:window-x="1920"
|
|
||||||
inkscape:window-y="1080"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1" />
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-102.65833,-145.78542)">
|
|
||||||
<title
|
|
||||||
id="title1">file-document-plus-outline</title>
|
|
||||||
<path
|
|
||||||
d="m 108.40358,150.62351 h -0.90715 v -0.90714 h -0.60476 v 0.90714 h -0.90715 v 0.60477 h 0.90715 v 0.90714 h 0.60476 v -0.90714 h 0.90715 m -5.14048,-5.44286 c -0.33565,0 -0.60477,0.27214 -0.60477,0.60475 v 4.83811 c 0,0.33563 0.26912,0.60475 0.60477,0.60475 h 2.3616 c -0.10892,-0.18747 -0.18446,-0.3931 -0.22075,-0.60475 h -2.14085 v -4.83811 h 2.11666 v 1.51191 h 1.51191 v 1.23372 c 0.0998,-0.0151 0.20259,-0.0242 0.30237,-0.0242 0.10286,0 0.2026,0.009 0.30239,0.0242 v -1.53609 l -1.81428,-1.81429 m -1.81429,3.02381 v 0.60476 h 2.41904 v -0.60476 m -2.41904,1.20952 v 0.60476 h 1.5119 v -0.60476 z"
|
|
||||||
id="path1"
|
|
||||||
style="stroke-width:0.302381" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB |
|
|
@ -25,11 +25,11 @@
|
||||||
inkscape:document-units="mm"
|
inkscape:document-units="mm"
|
||||||
inkscape:zoom="23.548693"
|
inkscape:zoom="23.548693"
|
||||||
inkscape:cx="6.9218279"
|
inkscape:cx="6.9218279"
|
||||||
inkscape:cy="12.612165"
|
inkscape:cy="12.5697"
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="1916"
|
||||||
inkscape:window-height="1161"
|
inkscape:window-height="1161"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
inkscape:window-y="18"
|
inkscape:window-y="0"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
showgrid="false" />
|
showgrid="false" />
|
||||||
|
|
@ -45,6 +45,6 @@
|
||||||
<path
|
<path
|
||||||
d="m 104.775,150.01875 a 1.5875,1.5875 0 0 1 -1.5875,-1.5875 c 0,-0.26458 0.0661,-0.52123 0.18521,-0.74083 l -0.38629,-0.3863 c -0.20638,0.32544 -0.32809,0.71173 -0.32809,1.12713 a 2.1166667,2.1166667 0 0 0 2.11667,2.11667 v 0.79375 l 1.05833,-1.05834 -1.05833,-1.05833 m 0,-2.91042 v -0.79375 l -1.05833,1.05834 1.05833,1.05833 v -0.79375 a 1.5875,1.5875 0 0 1 1.5875,1.5875 c 0,0.26458 -0.0661,0.52123 -0.18521,0.74083 l 0.38629,0.38629 c 0.20638,-0.32543 0.32809,-0.71172 0.32809,-1.12712 a 2.1166667,2.1166667 0 0 0 -2.11667,-2.11667 z"
|
d="m 104.775,150.01875 a 1.5875,1.5875 0 0 1 -1.5875,-1.5875 c 0,-0.26458 0.0661,-0.52123 0.18521,-0.74083 l -0.38629,-0.3863 c -0.20638,0.32544 -0.32809,0.71173 -0.32809,1.12713 a 2.1166667,2.1166667 0 0 0 2.11667,2.11667 v 0.79375 l 1.05833,-1.05834 -1.05833,-1.05833 m 0,-2.91042 v -0.79375 l -1.05833,1.05834 1.05833,1.05833 v -0.79375 a 1.5875,1.5875 0 0 1 1.5875,1.5875 c 0,0.26458 -0.0661,0.52123 -0.18521,0.74083 l 0.38629,0.38629 c 0.20638,-0.32543 0.32809,-0.71172 0.32809,-1.12712 a 2.1166667,2.1166667 0 0 0 -2.11667,-2.11667 z"
|
||||||
id="path1"
|
id="path1"
|
||||||
style="stroke-width:0.264583;fill:#000000;fill-opacity:1" />
|
style="stroke-width:0.264583;fill:#fe5f55;fill-opacity:1" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
|
@ -7,7 +7,7 @@
|
||||||
viewBox="0 0 4.7624998 4.7624998"
|
viewBox="0 0 4.7624998 4.7624998"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
sodipodi:docname="icon_save.svg"
|
sodipodi:docname="icon_save.svg"
|
||||||
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"
|
||||||
|
|
@ -24,12 +24,12 @@
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="mm"
|
inkscape:document-units="mm"
|
||||||
inkscape:zoom="5.6568542"
|
inkscape:zoom="5.6568542"
|
||||||
inkscape:cx="41.365747"
|
inkscape:cx="41.454135"
|
||||||
inkscape:cy="-3.7123106"
|
inkscape:cy="-3.8890873"
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="1093"
|
||||||
inkscape:window-height="1161"
|
inkscape:window-height="1401"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="2560"
|
||||||
inkscape:window-y="18"
|
inkscape:window-y="0"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1" />
|
inkscape:current-layer="layer1" />
|
||||||
<defs
|
<defs
|
||||||
|
|
@ -44,6 +44,6 @@
|
||||||
<path
|
<path
|
||||||
d="m 43.65625,209.81458 h -2.645833 v -1.05833 h 2.645833 m -0.79375,3.70417 a 0.79375,0.79375 0 0 1 -0.79375,-0.79375 0.79375,0.79375 0 0 1 0.79375,-0.79375 0.79375,0.79375 0 0 1 0.79375,0.79375 0.79375,0.79375 0 0 1 -0.79375,0.79375 m 1.322917,-4.23334 h -3.175 c -0.293688,0 -0.529167,0.23813 -0.529167,0.52917 v 3.70417 a 0.52916667,0.52916667 0 0 0 0.529167,0.52916 h 3.704166 a 0.52916667,0.52916667 0 0 0 0.529167,-0.52916 v -3.175 z"
|
d="m 43.65625,209.81458 h -2.645833 v -1.05833 h 2.645833 m -0.79375,3.70417 a 0.79375,0.79375 0 0 1 -0.79375,-0.79375 0.79375,0.79375 0 0 1 0.79375,-0.79375 0.79375,0.79375 0 0 1 0.79375,0.79375 0.79375,0.79375 0 0 1 -0.79375,0.79375 m 1.322917,-4.23334 h -3.175 c -0.293688,0 -0.529167,0.23813 -0.529167,0.52917 v 3.70417 a 0.52916667,0.52916667 0 0 0 0.529167,0.52916 h 3.704166 a 0.52916667,0.52916667 0 0 0 0.529167,-0.52916 v -3.175 z"
|
||||||
id="path1"
|
id="path1"
|
||||||
style="stroke-width:0.264583;fill:#000000;fill-opacity:1" />
|
style="stroke-width:0.264583;fill:#fe5f55;fill-opacity:1" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
|
@ -35,7 +35,7 @@
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="1916"
|
||||||
inkscape:window-height="1161"
|
inkscape:window-height="1161"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
inkscape:window-y="0"
|
inkscape:window-y="18"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:showpageshadow="true"
|
inkscape:showpageshadow="true"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
|
|
@ -62,6 +62,6 @@
|
||||||
<path
|
<path
|
||||||
d="m 78.736803,96.575592 a 40.634474,40.634474 0 0 1 40.634477,40.634438 c 0,10.06486 -3.68838,19.31694 -9.75227,26.44372 l 1.68795,1.68701 h 4.93863 l 31.2573,31.25736 -9.37719,9.37731 -31.25729,-31.25737 v -4.93863 l -1.68795,-1.68701 c -7.126666,6.06378 -16.378815,9.75204 -26.443681,9.75204 A 40.634474,40.634474 0 0 1 38.102322,137.21003 40.634474,40.634474 0 0 1 78.736803,96.575592 m 0,12.502758 c -15.628636,0 -28.131559,12.50299 -28.131559,28.13168 0,15.62868 12.502923,28.13144 28.131559,28.13144 15.628635,0 28.131557,-12.50276 28.131557,-28.13144 0,-15.62869 -12.502922,-28.13168 -28.131557,-28.13168 z"
|
d="m 78.736803,96.575592 a 40.634474,40.634474 0 0 1 40.634477,40.634438 c 0,10.06486 -3.68838,19.31694 -9.75227,26.44372 l 1.68795,1.68701 h 4.93863 l 31.2573,31.25736 -9.37719,9.37731 -31.25729,-31.25737 v -4.93863 l -1.68795,-1.68701 c -7.126666,6.06378 -16.378815,9.75204 -26.443681,9.75204 A 40.634474,40.634474 0 0 1 38.102322,137.21003 40.634474,40.634474 0 0 1 78.736803,96.575592 m 0,12.502758 c -15.628636,0 -28.131559,12.50299 -28.131559,28.13168 0,15.62868 12.502923,28.13144 28.131559,28.13144 15.628635,0 28.131557,-12.50276 28.131557,-28.13144 0,-15.62869 -12.502922,-28.13168 -28.131557,-28.13168 z"
|
||||||
id="path1"
|
id="path1"
|
||||||
style="stroke-width:6.25145;fill:#000000;fill-opacity:1" />
|
style="stroke-width:6.25145;fill:#fe5f55;fill-opacity:1" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
|
@ -1,49 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="5.89642mm"
|
|
||||||
height="6.3499999mm"
|
|
||||||
viewBox="0 0 5.89642 6.3499999"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
|
||||||
sodipodi:docname="icon_transfer.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:zoom="1"
|
|
||||||
inkscape:cx="9.5"
|
|
||||||
inkscape:cy="10"
|
|
||||||
inkscape:window-width="1916"
|
|
||||||
inkscape:window-height="1161"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1" />
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-102.39375,-145.78542)">
|
|
||||||
<title
|
|
||||||
id="title1">file-arrow-up-down-outline</title>
|
|
||||||
<path
|
|
||||||
d="m 105.14239,151.22828 c 0.0363,0.21771 0.11189,0.42031 0.21771,0.60475 h -2.36158 c -0.33565,0 -0.60477,-0.26912 -0.60477,-0.60475 v -4.83811 c 0,-0.33261 0.26912,-0.60475 0.60477,-0.60475 h 2.41904 l 1.81428,1.81429 v 1.53912 c -0.0998,-0.0151 -0.19956,-0.0272 -0.30238,-0.0272 -0.10285,0 -0.20259,0.0121 -0.30237,0.0272 v -1.23675 h -1.51191 v -1.51191 h -2.11666 v 4.83811 h 2.14387 m 1.18231,-1.51191 -0.75596,0.90714 h 0.45358 v 1.20952 h 0.60477 v -1.20952 h 0.45356 l -0.75595,-0.90714 m 1.51191,1.51191 v -1.20953 h -0.60477 v 1.20953 h -0.45356 l 0.75595,0.90714 0.75594,-0.90714 z"
|
|
||||||
id="path1"
|
|
||||||
style="stroke-width:0.302381;fill:#ffffff" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB |
|
|
@ -8,7 +8,7 @@
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
sodipodi:docname="leaf.svg"
|
sodipodi:docname="leaf.svg"
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||||
xml:space="preserve"
|
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"
|
||||||
|
|
@ -23,12 +23,12 @@
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:zoom="31.614857"
|
inkscape:zoom="11.17754"
|
||||||
inkscape:cx="5.0450964"
|
inkscape:cx="8.0965937"
|
||||||
inkscape:cy="9.5682862"
|
inkscape:cy="22.903072"
|
||||||
inkscape:window-width="2190"
|
inkscape:window-width="1916"
|
||||||
inkscape:window-height="1401"
|
inkscape:window-height="1161"
|
||||||
inkscape:window-x="1463"
|
inkscape:window-x="0"
|
||||||
inkscape:window-y="18"
|
inkscape:window-y="18"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
|
|
@ -42,10 +42,6 @@
|
||||||
id="title1-1">folder-open-outline</title><title
|
id="title1-1">folder-open-outline</title><title
|
||||||
id="title1-5">notebook-outline</title><title
|
id="title1-5">notebook-outline</title><title
|
||||||
id="title1-8">text-box-outline</title><path
|
id="title1-8">text-box-outline</title><path
|
||||||
style="fill:#ffffff;stroke-width:0.264583"
|
|
||||||
d="m 108.3015,148.56838 h 3.95851 v 3.96688 h -3.95851 z"
|
|
||||||
id="path3"
|
|
||||||
sodipodi:nodetypes="ccccc" /><path
|
|
||||||
d="m 108.47917,148.16667 c -0.29369,0 -0.52917,0.23548 -0.52917,0.52917 V 152.4 c 0,0.29369 0.23548,0.52917 0.52917,0.52917 h 3.70416 c 0.29369,0 0.52917,-0.23548 0.52917,-0.52917 v -3.70416 c 0,-0.29369 -0.23548,-0.52917 -0.52917,-0.52917 h -3.70416 m 0,0.52917 h 3.70416 V 152.4 h -3.70416 v -3.70416"
|
d="m 108.47917,148.16667 c -0.29369,0 -0.52917,0.23548 -0.52917,0.52917 V 152.4 c 0,0.29369 0.23548,0.52917 0.52917,0.52917 h 3.70416 c 0.29369,0 0.52917,-0.23548 0.52917,-0.52917 v -3.70416 c 0,-0.29369 -0.23548,-0.52917 -0.52917,-0.52917 h -3.70416 m 0,0.52917 h 3.70416 V 152.4 h -3.70416 v -3.70416"
|
||||||
id="path1"
|
id="path1"
|
||||||
style="fill:#ababab;fill-opacity:1;stroke-width:0.264583"
|
style="fill:#ababab;fill-opacity:1;stroke-width:0.264583"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
|
@ -1,68 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="24.000015"
|
|
||||||
height="23.999998"
|
|
||||||
viewBox="0 0 6.350004 6.3499995"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="leaf_deleted.svg"
|
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="22.627417"
|
|
||||||
inkscape:cx="3.292466"
|
|
||||||
inkscape:cy="15.62264"
|
|
||||||
inkscape:window-width="2190"
|
|
||||||
inkscape:window-height="1401"
|
|
||||||
inkscape:window-x="1463"
|
|
||||||
inkscape:window-y="18"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false" /><defs
|
|
||||||
id="defs1" /><g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-107.95,-148.16667)"><title
|
|
||||||
id="title1">folder-open</title><title
|
|
||||||
id="title1-1">folder-open-outline</title><title
|
|
||||||
id="title1-5">notebook-outline</title><title
|
|
||||||
id="title1-8">text-box-outline</title><path
|
|
||||||
style="fill:#ffffff;stroke-width:0.264583"
|
|
||||||
d="m 108.3015,148.56838 h 3.95851 v 3.96688 h -3.95851 z"
|
|
||||||
id="path3"
|
|
||||||
sodipodi:nodetypes="ccccc" /><path
|
|
||||||
d="m 108.47917,148.16667 c -0.29369,0 -0.52917,0.23548 -0.52917,0.52917 V 152.4 c 0,0.29369 0.23548,0.52917 0.52917,0.52917 h 3.70416 c 0.29369,0 0.52917,-0.23548 0.52917,-0.52917 v -3.70416 c 0,-0.29369 -0.23548,-0.52917 -0.52917,-0.52917 h -3.70416 m 0,0.52917 h 3.70416 V 152.4 h -3.70416 v -3.70416"
|
|
||||||
id="path1"
|
|
||||||
style="fill:#ababab;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
sodipodi:nodetypes="cssssssscccccc" /><path
|
|
||||||
d="m 109.00833,149.225 v 0.52917 h 2.64584 V 149.225 h -2.64584 m 0,1.05834 v 0.52916 h 2.64584 v -0.52916 h -2.64584 m 0,1.05833 v 0.52917 h 1.85209 v -0.52917 z"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#c7c7c7;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
sodipodi:nodetypes="ccccccccccccccc" /><title
|
|
||||||
id="title1-9">delete-circle</title><g
|
|
||||||
id="g1"
|
|
||||||
transform="matrix(1.6249303,0,0,1.6249427,-68.307567,-93.38766)"><rect
|
|
||||||
style="fill:#ffffff;stroke:none;stroke-width:0.79375"
|
|
||||||
id="rect1"
|
|
||||||
width="1.5817325"
|
|
||||||
height="1.8579081"
|
|
||||||
x="110.28522"
|
|
||||||
y="150.33034" /><path
|
|
||||||
d="m 111.0761,149.95667 c 0.72034,0 1.30261,0.58227 1.30261,1.30262 0,0.72035 -0.58227,1.3026 -1.30261,1.3026 -0.72035,0 -1.30263,-0.58225 -1.30263,-1.3026 0,-0.72035 0.58227,-1.30262 1.30263,-1.30262 m 0.6513,0.65131 h -0.32566 l -0.13026,-0.13026 h -0.39078 l -0.13027,0.13026 h -0.32565 v 0.26052 h 1.30262 v -0.26052 m -1.0421,1.43288 h 0.78157 a 0.13026143,0.13026143 0 0 0 0.13026,-0.13026 v -0.91184 h -1.04209 v 0.91184 a 0.13026143,0.13026143 0 0 0 0.13026,0.13026 z"
|
|
||||||
id="path1-1"
|
|
||||||
style="fill:#aa0000;stroke-width:0.130261" /></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
|
@ -1,61 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="24.000015"
|
|
||||||
height="23.999998"
|
|
||||||
viewBox="0 0 6.350004 6.3499995"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="leaf_orphaned.svg"
|
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="22.627417"
|
|
||||||
inkscape:cx="3.2924659"
|
|
||||||
inkscape:cy="15.600543"
|
|
||||||
inkscape:window-width="1916"
|
|
||||||
inkscape:window-height="1161"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false" /><defs
|
|
||||||
id="defs1" /><g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-107.95,-148.16667)"><title
|
|
||||||
id="title1">folder-open</title><title
|
|
||||||
id="title1-1">folder-open-outline</title><title
|
|
||||||
id="title1-5">notebook-outline</title><title
|
|
||||||
id="title1-8">text-box-outline</title><path
|
|
||||||
style="fill:#ffffff;stroke-width:0.264583"
|
|
||||||
d="m 108.3015,148.56838 h 3.95851 v 3.96688 h -3.95851 z"
|
|
||||||
id="path3"
|
|
||||||
sodipodi:nodetypes="ccccc" /><path
|
|
||||||
d="m 108.47917,148.16667 c -0.29369,0 -0.52917,0.23548 -0.52917,0.52917 V 152.4 c 0,0.29369 0.23548,0.52917 0.52917,0.52917 h 3.70416 c 0.29369,0 0.52917,-0.23548 0.52917,-0.52917 v -3.70416 c 0,-0.29369 -0.23548,-0.52917 -0.52917,-0.52917 h -3.70416 m 0,0.52917 h 3.70416 V 152.4 h -3.70416 v -3.70416"
|
|
||||||
id="path1"
|
|
||||||
style="fill:#ababab;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
sodipodi:nodetypes="cssssssscccccc" /><path
|
|
||||||
d="m 109.00833,149.225 v 0.52917 h 2.64584 V 149.225 h -2.64584 m 0,1.05834 v 0.52916 h 2.64584 v -0.52916 h -2.64584 m 0,1.05833 v 0.52917 h 1.85209 v -0.52917 z"
|
|
||||||
id="path2"
|
|
||||||
style="fill:#c7c7c7;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
sodipodi:nodetypes="ccccccccccccccc" /><title
|
|
||||||
id="title1-9">delete-circle</title><title
|
|
||||||
id="title1-53">ghost</title><path
|
|
||||||
d="m 112.39499,150.28334 a 1.9050024,1.9050024 0 0 0 -1.905,1.905 v 2.32833 l 0.635,-0.635 0.635,0.635 0.635,-0.635 0.635,0.635 0.635,-0.635 0.63501,0.635 v -2.32833 a 1.9050024,1.9050024 0 0 0 -1.90501,-1.905 m -0.635,1.27 a 0.42333387,0.42333387 0 0 1 0.42334,0.42333 0.42333387,0.42333387 0 0 1 -0.42334,0.42334 0.42333387,0.42333387 0 0 1 -0.42333,-0.42334 0.42333387,0.42333387 0 0 1 0.42333,-0.42333 m 1.27,0 a 0.42333387,0.42333387 0 0 1 0.42334,0.42333 0.42333387,0.42333387 0 0 1 -0.42334,0.42334 0.42333387,0.42333387 0 0 1 -0.42333,-0.42334 0.42333387,0.42333387 0 0 1 0.42333,-0.42333 z"
|
|
||||||
id="path1-5"
|
|
||||||
style="fill:#005190;fill-opacity:1;stroke-width:0.264583" /></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
|
@ -7,7 +7,7 @@
|
||||||
viewBox="0 0 7.5652731 5.2916666"
|
viewBox="0 0 7.5652731 5.2916666"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
sodipodi:docname="logo_small.svg"
|
sodipodi:docname="logo_small.svg"
|
||||||
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"
|
||||||
|
|
@ -23,13 +23,13 @@
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:zoom="8.2386085"
|
inkscape:zoom="16.477217"
|
||||||
inkscape:cx="48.491198"
|
inkscape:cx="48.460855"
|
||||||
inkscape:cy="5.219328"
|
inkscape:cy="5.2193281"
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="2190"
|
||||||
inkscape:window-height="1161"
|
inkscape:window-height="1401"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="1463"
|
||||||
inkscape:window-y="0"
|
inkscape:window-y="18"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1" />
|
inkscape:current-layer="layer1" />
|
||||||
<defs
|
<defs
|
||||||
|
|
@ -47,23 +47,17 @@
|
||||||
x="126.73541"
|
x="126.73541"
|
||||||
y="178.59375"
|
y="178.59375"
|
||||||
ry="1.0060203" />
|
ry="1.0060203" />
|
||||||
<path
|
|
||||||
d="m 114.29355,207.99469 h -0.57869 l -0.72335,-1.79391 v 1.79391 h -0.49188 v -3.03326 h 0.50153 l 0.79568,1.99645 v -1.99645 h 0.49671 z m 2.34365,-0.50635 v 0.50635 h -1.85178 v -0.99823 q 0,-0.78122 0.72817,-0.78122 h 0.48224 q 0.16396,-0.005 0.16396,-0.18325 v -0.40507 q 0,-0.17361 -0.13985,-0.17361 h -0.64137 q -0.0964,0 -0.0964,0.10609 v 0.21701 h -0.4967 v -0.14949 q 0,-0.67996 0.59797,-0.67996 h 0.59797 q 0.65584,0 0.65584,0.63655 v 0.43402 q 0,0.33756 -0.15431,0.52081 h 0.005 q -0.15432,0.18325 -0.6028,0.18325 h -0.40507 q -0.19772,0 -0.19772,0.25076 v 0.51599 z"
|
|
||||||
id="text5"
|
|
||||||
style="font-weight:bold;font-size:4.82235px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Bold';fill:#ffffff;stroke-width:0.40186"
|
|
||||||
transform="scale(1.1392149,0.87779751)"
|
|
||||||
aria-label="N2" />
|
|
||||||
<text
|
<text
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.82235px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:none;stroke-width:0.40186"
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.82235px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:none;stroke-width:0.40186"
|
||||||
x="112.25369"
|
x="112.25369"
|
||||||
y="213.81186"
|
y="207.99469"
|
||||||
id="text1"
|
id="text5"
|
||||||
transform="scale(1.1392149,0.87779751)"><tspan
|
transform="scale(1.1392149,0.87779751)"><tspan
|
||||||
sodipodi:role="line"
|
sodipodi:role="line"
|
||||||
id="tspan1"
|
id="tspan5"
|
||||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.82235px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;stroke:none;stroke-width:0.40186"
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.82235px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;stroke:none;stroke-width:0.40186"
|
||||||
x="112.25369"
|
x="112.25369"
|
||||||
y="213.81186">N2</tspan></text>
|
y="207.99469">N2</tspan></text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.5 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 })
|
||||||
// An HTTP communication level error occured.
|
.then(response => {
|
||||||
if (!res.ok || res.status != 200)
|
// An HTTP communication level error occured.
|
||||||
throw new Error('HTTP error', { cause: { type: 'http', error: res, }})
|
if (!response.ok || response.status != 200)
|
||||||
|
return reject({
|
||||||
// Application level response are handled here.
|
type: 'http',
|
||||||
const json = await res.json()
|
error: response,
|
||||||
if (!json.OK)
|
})
|
||||||
throw new Error(json.Error, { cause: { type: 'application', application: json, }})
|
return response.json()
|
||||||
|
})
|
||||||
return json
|
.then(json => {
|
||||||
|
// Application level response are handled here.
|
||||||
} catch (err) {
|
if (!json.OK)
|
||||||
// Catch any other errors from fetch.
|
return reject({
|
||||||
throw new Error(err.message, { cause: { type: 'http', error: err, }})
|
type: 'application',
|
||||||
}
|
error: json.Error,
|
||||||
|
application: json,
|
||||||
|
})
|
||||||
|
resolve(json)
|
||||||
|
})
|
||||||
|
.catch(err =>
|
||||||
|
// Catch any other errors from fetch.
|
||||||
|
reject({
|
||||||
|
type: 'http',
|
||||||
|
error: err,
|
||||||
|
}))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static hasAuthenticationToken() {//{{{
|
static hasAuthenticationToken() {//{{{
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,40 @@
|
||||||
import { ROOT_NODE } from 'node_store'
|
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 { N2Tree } from 'tree'
|
||||||
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']
|
|
||||||
|
|
||||||
constructor() {// {{{
|
constructor() {// {{{
|
||||||
this.currentNode = null
|
this.currentNode = null
|
||||||
this.sidebar = new N2Sidebar()
|
this.tree = new N2Tree()
|
||||||
this.crumbs = new N2Crumbs()
|
this.crumbs = new N2Crumbs()
|
||||||
this.crumbsElement = document.getElementById('crumbs')
|
this.crumbsElement = document.getElementById('crumbs')
|
||||||
this.nodeUI = document.getElementById('note')
|
this.nodeUI = document.getElementById('note')
|
||||||
this.dragIcon = new N2DragIcon()
|
|
||||||
|
|
||||||
this.preferences = this.getPreferences()
|
_mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
|
||||||
|
document.getElementById('tree').append(this.tree.render())
|
||||||
this.sidebar.render().then(sidebar => {
|
|
||||||
document.getElementById('tree').append(sidebar)
|
|
||||||
document.getElementById('tree-nodes')?.focus()
|
document.getElementById('tree-nodes')?.focus()
|
||||||
})
|
|
||||||
|
|
||||||
// Start node shows a system-wide page instead of node editing
|
|
||||||
// since the start node is kind of magic and doesn't fit into
|
|
||||||
// the syncing system.
|
|
||||||
const determineNodePage = uuid => {
|
|
||||||
const el = document.getElementById('notes2')
|
|
||||||
if (uuid == ROOT_NODE)
|
|
||||||
el.classList.add('root-node-override')
|
|
||||||
else
|
|
||||||
el.classList.remove('root-node-override')
|
|
||||||
}
|
|
||||||
|
|
||||||
_mbus.subscribe('TREE_RENDERED', async () => {
|
|
||||||
// Subscribing to the start node existing after the tree trunk is
|
|
||||||
// fetched since the NODE_COMPONENT_EXIST message isn't sent for the
|
|
||||||
// root node itself, and the root node should be selected in the tree
|
|
||||||
// after it is rendered when the site is shown without UUID in the URL.
|
|
||||||
const startNode = await this.getStartNode()
|
const startNode = await this.getStartNode()
|
||||||
determineNodePage(startNode.UUID)
|
|
||||||
this.goToNode(startNode.UUID, false, false)
|
this.goToNode(startNode.UUID, false, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
_mbus.subscribe('TREE_NODE_SELECTED', event => {
|
_mbus.subscribe('TREE_NODE_SELECTED', event => {
|
||||||
const node = event.detail.data
|
const node = event.detail.data
|
||||||
determineNodePage(node.UUID)
|
|
||||||
this.goToNode(node.UUID, false, false)
|
this.goToNode(node.UUID, false, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
_mbus.subscribe('GO_TO_NODE', event => {
|
_mbus.subscribe('GO_TO_NODE', event => {
|
||||||
const node = event.detail.data
|
const node = event.detail.data
|
||||||
determineNodePage(node.nodeUUID)
|
|
||||||
this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
|
this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
|
||||||
})
|
})
|
||||||
|
|
||||||
_mbus.subscribe('SHOW_PAGE', ({ detail: { data: { page } } }) => {
|
_mbus.subscribe('SHOW_PAGE', ({ detail: { data: { page } } }) => {
|
||||||
const classList = document.getElementById('notes2').classList
|
const classList = document.querySelector('#main-page').classList
|
||||||
classList.forEach(e => {
|
classList.forEach(e =>
|
||||||
if (e.startsWith('page-'))
|
classList.remove(e)
|
||||||
classList.remove(e)
|
)
|
||||||
})
|
classList.add(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))
|
||||||
|
|
@ -76,9 +44,6 @@ export class App {
|
||||||
document.getElementById('node-content')?.focus()
|
document.getElementById('node-content')?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.querySelector('#page-root .create').addEventListener('click', () => this.createNode())
|
|
||||||
document.body.append(this.dragIcon)
|
|
||||||
|
|
||||||
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
|
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
|
||||||
|
|
||||||
window._sync = new Sync()
|
window._sync = new Sync()
|
||||||
|
|
@ -88,53 +53,71 @@ export class App {
|
||||||
// There a slight delay to initiate sync seems reasonable.
|
// There a slight delay to initiate sync seems reasonable.
|
||||||
setTimeout(() => window._sync.run(), 1000)
|
setTimeout(() => window._sync.run(), 1000)
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
keyHandler(event) {//{{{
|
keyHandler(event) {//{{{
|
||||||
let handled = true
|
let handled = true
|
||||||
|
|
||||||
// Most keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
|
// All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
|
||||||
// Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving.
|
// Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving.
|
||||||
// Thus, the exception is acceptable to consequent use of alt+shift.
|
// Thus, the exception is acceptable to consequent use of alt+shift.
|
||||||
const CTRL = !event.shiftKey && event.ctrlKey && !event.altKey
|
if (!(event.shiftKey && event.altKey) && !(event.key.toUpperCase() === 'S' && event.ctrlKey))
|
||||||
const SHIFT_ALT = event.shiftKey && !event.ctrlKey && event.altKey
|
return
|
||||||
const SHIFT_CTRL_ALT = event.shiftKey && event.ctrlKey && event.altKey
|
|
||||||
|
|
||||||
switch (event.key.toUpperCase()) {
|
switch (event.key.toUpperCase()) {
|
||||||
case 'F2':
|
|
||||||
this.nodeUI.renameNode()
|
|
||||||
break
|
|
||||||
case 'T':
|
case 'T':
|
||||||
if (!SHIFT_ALT) { handled = false; break }
|
if (document.activeElement.id === 'tree-nodes') {
|
||||||
if (document.activeElement.id === 'tree-nodes')
|
console.log('take focus')
|
||||||
this.nodeUI.takeFocus()
|
this.nodeUI.takeFocus()
|
||||||
else
|
} else {
|
||||||
this.sidebar.focus()
|
this.tree.focus()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'F':
|
case 'F':
|
||||||
if (!SHIFT_ALT) { handled = false; break }
|
|
||||||
_mbus.dispatch('op-search')
|
_mbus.dispatch('op-search')
|
||||||
break
|
break
|
||||||
|
/*
|
||||||
|
case 'C':
|
||||||
|
this.showPage('node')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'E':
|
||||||
|
this.showPage('keys')
|
||||||
|
break
|
||||||
|
*/
|
||||||
|
|
||||||
case 'M':
|
case 'M':
|
||||||
if (!SHIFT_ALT) { handled = false; break }
|
|
||||||
globalThis._mbus.dispatch('MARKDOWN_TOGGLE')
|
globalThis._mbus.dispatch('MARKDOWN_TOGGLE')
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'N':
|
case 'N':
|
||||||
if (SHIFT_ALT)
|
this.createNode()
|
||||||
this.createNode()
|
|
||||||
else if (SHIFT_CTRL_ALT) {
|
|
||||||
this.createNode(this.currentNode?.ParentUUID)
|
|
||||||
} else {
|
|
||||||
handled = false
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'S':
|
/*
|
||||||
if (!CTRL) { handled = false; break }
|
case 'P':
|
||||||
this.nodeUI.saveNode()
|
this.showPage('node-properties')
|
||||||
break
|
break
|
||||||
|
|
||||||
|
*/
|
||||||
|
case 'S':
|
||||||
|
this.saveNode()
|
||||||
|
/*
|
||||||
|
else if (this.page.value === 'node-properties')
|
||||||
|
this.nodeProperties.current.save()
|
||||||
|
*/
|
||||||
|
break
|
||||||
|
/*
|
||||||
|
|
||||||
|
case 'U':
|
||||||
|
this.showPage('upload')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'F':
|
||||||
|
this.showPage('search')
|
||||||
|
break
|
||||||
|
*/
|
||||||
|
|
||||||
default:
|
default:
|
||||||
handled = false
|
handled = false
|
||||||
}
|
}
|
||||||
|
|
@ -158,28 +141,47 @@ export class App {
|
||||||
return await nodeStore.get(nodeUUID)
|
return await nodeStore.get(nodeUUID)
|
||||||
}//}}}
|
}//}}}
|
||||||
async saveNode() {//{{{
|
async saveNode() {//{{{
|
||||||
|
if (!this.currentNode.isModified())
|
||||||
|
return
|
||||||
|
|
||||||
}//}}}
|
/* The node history is a local store for node history.
|
||||||
async moveNode(node, targetNodeUUID) {// {{{
|
* This could be provisioned from the server or cleared if
|
||||||
node.moveToParent(targetNodeUUID)
|
* deemed unnecessary.
|
||||||
|
*
|
||||||
|
* The send queue is what will be sent back to the server
|
||||||
|
* to have a recorded history of the notes.
|
||||||
|
*
|
||||||
|
* A setting to be implemented in the future could be to
|
||||||
|
* not save the history locally at all. */
|
||||||
|
const node = this.currentNode
|
||||||
|
|
||||||
|
// The node is still in its old state and will present
|
||||||
|
// the unmodified content to the node store.
|
||||||
|
const history = nodeStore.nodesHistory.add(node)
|
||||||
|
|
||||||
|
// Prepares the node object for saving.
|
||||||
|
// Sets Updated value to current date and time.
|
||||||
await node.save()
|
await node.save()
|
||||||
}// }}}
|
|
||||||
async createNode(createUnderUUID) {//{{{
|
|
||||||
const parentUUID = createUnderUUID ? createUnderUUID : this.currentNode.UUID
|
|
||||||
const p = createUnderUUID ? 'Name for sibling document' : 'Name for sub-document'
|
|
||||||
|
|
||||||
let name = prompt(p)
|
// Updated node is added to the send queue to be stored on server.
|
||||||
|
const sendQueue = nodeStore.sendQueue.add(node)
|
||||||
|
|
||||||
|
// Updated node is saved to the primary node store.
|
||||||
|
const nodeStoreAdding = nodeStore.add([node])
|
||||||
|
|
||||||
|
await Promise.all([history, sendQueue, nodeStoreAdding])
|
||||||
|
}//}}}
|
||||||
|
async createNode() {//{{{
|
||||||
|
let name = prompt("Name")
|
||||||
if (!name)
|
if (!name)
|
||||||
return
|
return
|
||||||
|
|
||||||
const nn = Node.create(name, parentUUID)
|
const nn = Node.create(name, this.currentNode.UUID)
|
||||||
await nn.save()
|
nn.save()
|
||||||
|
|
||||||
|
nodeStore.sendQueue.add(nn)
|
||||||
|
nodeStore.add([nn])
|
||||||
|
|
||||||
// Treenode is forcefully rerendered and children refetched to both show the new node
|
|
||||||
// and to get it resorted.
|
|
||||||
const parentTreenode = this.sidebar.getTreeNode(parentUUID)
|
|
||||||
await parentTreenode.render(true, true)
|
|
||||||
_mbus.dispatch('GO_TO_NODE', { nodeUUID: nn.UUID })
|
|
||||||
}//}}}
|
}//}}}
|
||||||
async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
|
async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
|
||||||
if (nodeUUID === null || nodeUUID === undefined)
|
if (nodeUUID === null || nodeUUID === undefined)
|
||||||
|
|
@ -198,33 +200,16 @@ export class App {
|
||||||
node.reset() // any modifications are discarded.
|
node.reset() // any modifications are discarded.
|
||||||
|
|
||||||
this.currentNode = node
|
this.currentNode = node
|
||||||
this.sidebar.setSelected(node, dontExpand)
|
this.tree.setSelected(node, dontExpand)
|
||||||
|
|
||||||
const ancestors = await nodeStore.getNodeAncestry(node)
|
const ancestors = await nodeStore.getNodeAncestry(node)
|
||||||
|
|
||||||
// Scrolls node into view.
|
|
||||||
// makeVisible normally expands all ancestor nodes to make the whole chain visible.
|
|
||||||
// This is a bad idea when quickly navigating the tree, since the arrow navigation
|
|
||||||
// has collapsed nodes which the event calling goToNode can come to undo, if the
|
|
||||||
// event processing lags behind.
|
|
||||||
await this.sidebar.makeVisible(node, ancestors, dontExpand)
|
|
||||||
|
|
||||||
_mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render()))
|
_mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render()))
|
||||||
_mbus.dispatch('NODE_UI_OPEN', node)
|
_mbus.dispatch('NODE_UI_OPEN', node)
|
||||||
_mbus.dispatch('TREE_EXPANSION', { expand: false, when: 'narrow' })
|
|
||||||
_mbus.dispatch('NODE_UNMODIFIED')
|
_mbus.dispatch('NODE_UNMODIFIED')
|
||||||
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
|
|
||||||
|
// Scrolls node into view.
|
||||||
|
this.tree.makeVisible(node)
|
||||||
}//}}}
|
}//}}}
|
||||||
pageIsVisible(page) {// {{{
|
|
||||||
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 {
|
class N2Crumbs extends CustomHTMLElement {
|
||||||
|
|
@ -251,13 +236,14 @@ class N2Crumbs extends CustomHTMLElement {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const start = new N2Crumb('', ROOT_NODE)
|
const start = new N2Crumb('Start', ROOT_NODE)
|
||||||
crumbs.push(start)
|
crumbs.push(start)
|
||||||
|
|
||||||
this.replaceChildren(...crumbs.reverse())
|
this.replaceChildren(...crumbs.reverse())
|
||||||
return this
|
return this
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
customElements.define('n2-crumbs', N2Crumbs)
|
||||||
|
|
||||||
class N2Crumb extends CustomHTMLElement {
|
class N2Crumb extends CustomHTMLElement {
|
||||||
static {// {{{
|
static {// {{{
|
||||||
|
|
@ -268,18 +254,8 @@ class N2Crumb extends CustomHTMLElement {
|
||||||
}// }}}
|
}// }}}
|
||||||
constructor(label, uuid) {// {{{
|
constructor(label, uuid) {// {{{
|
||||||
super()
|
super()
|
||||||
|
|
||||||
// The house makes it a bit more graphical than just a bunch of text.
|
|
||||||
if (uuid === ROOT_NODE) {
|
|
||||||
const start = document.createElement('div')
|
|
||||||
start.innerHTML = `<img src="/images/${_VERSION}/icon_home.svg">`
|
|
||||||
start.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: ROOT_NODE, dontPush: false, dontExpand: true }))
|
|
||||||
this.classList.add('home')
|
|
||||||
this.replaceChildren(start)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.classList.add('crumb')
|
this.classList.add('crumb')
|
||||||
|
|
||||||
this.label = label
|
this.label = label
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
|
|
||||||
|
|
@ -288,6 +264,7 @@ class N2Crumb extends CustomHTMLElement {
|
||||||
this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true }))
|
this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true }))
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
customElements.define('n2-crumb', N2Crumb)
|
||||||
|
|
||||||
function tmpl(html) {// {{{
|
function tmpl(html) {// {{{
|
||||||
const el = document.createElement('template')
|
const el = document.createElement('template')
|
||||||
|
|
@ -361,52 +338,4 @@ class OpSearch extends Op {
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class N2DragIcon extends CustomHTMLElement {
|
|
||||||
static {// {{{
|
|
||||||
this.tmpl = document.createElement('template')
|
|
||||||
this.tmpl.innerHTML = `
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 16384;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<img data-el="icon" src="/images/${_VERSION}/icon_drag.svg">
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
constructor() {// {{{
|
|
||||||
super(true)
|
|
||||||
|
|
||||||
document.addEventListener('dragover', e => {
|
|
||||||
this.style.left = `${e.clientX + 8}px`
|
|
||||||
this.style.top = `${e.clientY}px`
|
|
||||||
})
|
|
||||||
|
|
||||||
this.dragSource = null
|
|
||||||
}// }}}
|
|
||||||
start() {// {{{
|
|
||||||
this.style.display = 'block'
|
|
||||||
}// }}}
|
|
||||||
end() {// {{{
|
|
||||||
this.style.display = 'none'
|
|
||||||
}// }}}
|
|
||||||
icon(name) {// {{{
|
|
||||||
if (name != '')
|
|
||||||
name = '_' + name
|
|
||||||
this.elIcon.setAttribute('src', `/images/${_VERSION}/icon_drag${name}.svg`)
|
|
||||||
}// }}}
|
|
||||||
setSource(s) {// {{{
|
|
||||||
this.dragSource = s
|
|
||||||
}// }}}
|
|
||||||
getSource() {// {{{
|
|
||||||
return this.dragSource
|
|
||||||
}// }}}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('n2-crumbs', N2Crumbs)
|
|
||||||
customElements.define('n2-crumb', N2Crumb)
|
|
||||||
customElements.define('n2-dragicon', N2DragIcon)
|
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,9 +92,7 @@ function escapeHtmlEntities(html, encode) {// {{{
|
||||||
|
|
||||||
export class MarkedPosition {
|
export class MarkedPosition {
|
||||||
constructor() {// {{{
|
constructor() {// {{{
|
||||||
window.marked_setpos = (event) => this.setpos(event)
|
window.setpos = (event) => this.setpos(event)
|
||||||
window.marked_changecheckbox = (event) => this.changecheckbox(event)
|
|
||||||
window.marked_copy_to_clipboard = (event, tagname) => this.copy_to_clipboard(event, tagname)
|
|
||||||
this.render()
|
this.render()
|
||||||
}// }}}
|
}// }}}
|
||||||
setpos(event) {// {{{
|
setpos(event) {// {{{
|
||||||
|
|
@ -108,61 +106,20 @@ export class MarkedPosition {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
changecheckbox(event) {// {{{
|
|
||||||
event.stopPropagation()
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
_mbus.dispatch('MARKDOWN_CHANGE_CHECKBOX', {
|
|
||||||
checkbox: event.target,
|
|
||||||
position: {
|
|
||||||
start: event.target.closest('[data-offset-start]').dataset.offsetStart,
|
|
||||||
end: event.target.closest('[data-offset-start]').dataset.offsetEnd,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}// }}}
|
|
||||||
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({
|
||||||
renderer: {
|
renderer: {
|
||||||
heading(token) {
|
heading(token) {
|
||||||
const content = this.parser.parseInline(token.tokens)
|
const content = this.parser.parseInline(token.tokens)
|
||||||
return `
|
return `<h${token.depth} ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</h${token.depth}>\n`
|
||||||
<div class="heading-container" data-heading="${token.depth}">
|
|
||||||
<h${token.depth} ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</h${token.depth}>\n
|
|
||||||
<div class="line"></div>\n
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
},
|
},
|
||||||
|
|
||||||
paragraph(token) {
|
paragraph(token) {
|
||||||
const content = this.parser.parseInline(token.tokens)
|
const content = this.parser.parseInline(token.tokens)
|
||||||
return `<p ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</p>\n`
|
return `<p ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</p>\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
list(token) {
|
list(token) {
|
||||||
|
|
@ -181,7 +138,7 @@ export class MarkedPosition {
|
||||||
},
|
},
|
||||||
|
|
||||||
listitem(token) {
|
listitem(token) {
|
||||||
return `<li ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parse(token.tokens)}</li>\n`
|
return `<li ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parse(token.tokens)}</li>\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
code(token) {
|
code(token) {
|
||||||
|
|
@ -190,12 +147,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="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="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))
|
||||||
|
|
@ -204,7 +161,7 @@ export class MarkedPosition {
|
||||||
|
|
||||||
blockquote(token) {
|
blockquote(token) {
|
||||||
const body = this.parser.parse(token.tokens)
|
const body = this.parser.parse(token.tokens)
|
||||||
return `<blockquote ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n${body}</blockquote>\n`
|
return `<blockquote ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n${body}</blockquote>\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
html(token) {
|
html(token) {
|
||||||
|
|
@ -216,13 +173,13 @@ export class MarkedPosition {
|
||||||
},
|
},
|
||||||
|
|
||||||
hr(token) {
|
hr(token) {
|
||||||
return `<hr ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n`
|
return `<hr ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
checkbox(token) {
|
checkbox(token) {
|
||||||
return `<input ondblclick="marked_setpos(event)" onchange="marked_changecheckbox(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"`
|
return `<input ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"`
|
||||||
+ (token.checked ? 'checked="" ' : '')
|
+ (token.checked ? 'checked="" ' : '')
|
||||||
+ 'type="checkbox"> '
|
+ 'disabled="" type="checkbox"> '
|
||||||
},
|
},
|
||||||
|
|
||||||
table(token) {
|
table(token) {
|
||||||
|
|
@ -265,7 +222,7 @@ export class MarkedPosition {
|
||||||
if (token.tokens.length > 0) {
|
if (token.tokens.length > 0) {
|
||||||
const start = token.tokens[0].position.start.offset
|
const start = token.tokens[0].position.start.offset
|
||||||
const end = token.tokens[0].position.end.offset
|
const end = token.tokens[0].position.end.offset
|
||||||
ofs = `ondblclick="marked_setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
|
ofs = `ondblclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = this.parser.parseInline(token.tokens);
|
const content = this.parser.parseInline(token.tokens);
|
||||||
|
|
@ -277,23 +234,23 @@ export class MarkedPosition {
|
||||||
},
|
},
|
||||||
|
|
||||||
strong(token) {
|
strong(token) {
|
||||||
return `<strong ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</strong>`
|
return `<strong ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</strong>`
|
||||||
},
|
},
|
||||||
|
|
||||||
em(token) {
|
em(token) {
|
||||||
return `<em ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</em>`
|
return `<em ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</em>`
|
||||||
},
|
},
|
||||||
|
|
||||||
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="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) {
|
||||||
return `<br ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">`
|
return `<br ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">`
|
||||||
},
|
},
|
||||||
|
|
||||||
del(token) {
|
del(token) {
|
||||||
return `<del ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</del>`
|
return `<del ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</del>`
|
||||||
},
|
},
|
||||||
|
|
||||||
link(token) {
|
link(token) {
|
||||||
|
|
@ -303,7 +260,7 @@ export class MarkedPosition {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
token.href = cleanHref
|
token.href = cleanHref
|
||||||
let out = '<a ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" href="' + token.href + '"'
|
let out = '<a ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" href="' + token.href + '"'
|
||||||
if (token.title) {
|
if (token.title) {
|
||||||
out += ' title="' + (escapeHtmlEntities(token.title)) + '"'
|
out += ' title="' + (escapeHtmlEntities(token.title)) + '"'
|
||||||
}
|
}
|
||||||
|
|
@ -320,7 +277,7 @@ export class MarkedPosition {
|
||||||
return escapeHtmlEntities(token.text)
|
return escapeHtmlEntities(token.text)
|
||||||
}
|
}
|
||||||
token.href = cleanHref
|
token.href = cleanHref
|
||||||
let out = `<n2-file ondblclick="marked_setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" src="${token.href}" alt="${escapeHtmlEntities(token.text)}"`
|
let out = `<n2-file ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" src="${token.href}" alt="${escapeHtmlEntities(token.text)}"`
|
||||||
if (token.title) {
|
if (token.title) {
|
||||||
out += ` title="${escapeHtmlEntities(token.title)}"`
|
out += ` title="${escapeHtmlEntities(token.title)}"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { Node } from 'node'
|
import { Node } from 'node'
|
||||||
|
|
||||||
export const ROOT_NODE = '00000000-0000-0000-0000-000000000000'
|
export const ROOT_NODE = '00000000-0000-0000-0000-000000000000'
|
||||||
export const ORPHANED_NODE = '00000000-0000-0000-0000-000000000001'
|
|
||||||
export const DELETED_NODE = '00000000-0000-0000-0000-000000000002'
|
|
||||||
|
|
||||||
export class NodeStore {
|
export class NodeStore {
|
||||||
constructor() {//{{{
|
constructor() {//{{{
|
||||||
|
|
@ -15,8 +13,6 @@ export class NodeStore {
|
||||||
this.sendQueue = null
|
this.sendQueue = null
|
||||||
this.nodesHistory = null
|
this.nodesHistory = null
|
||||||
this.files = null
|
this.files = null
|
||||||
|
|
||||||
this.initializeSpecialNodes()
|
|
||||||
}//}}}
|
}//}}}
|
||||||
initializeDB() {//{{{
|
initializeDB() {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
@ -61,7 +57,7 @@ export class NodeStore {
|
||||||
break
|
break
|
||||||
|
|
||||||
case 6:
|
case 6:
|
||||||
nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'HistoryUUID'] })
|
nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'Updated'] })
|
||||||
break
|
break
|
||||||
|
|
||||||
case 7:
|
case 7:
|
||||||
|
|
@ -78,9 +74,10 @@ export class NodeStore {
|
||||||
req.onsuccess = (event) => {
|
req.onsuccess = (event) => {
|
||||||
this.db = event.target.result
|
this.db = event.target.result
|
||||||
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
|
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
|
||||||
this.nodesHistory = new NodeHistoryStore(this.db, 'nodes_history')
|
this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history')
|
||||||
this.files = new SimpleNodeStore(this.db, 'files')
|
this.files = new SimpleNodeStore(this.db, 'files')
|
||||||
resolve()
|
this.initializeRootNode()
|
||||||
|
.then(() => resolve())
|
||||||
}
|
}
|
||||||
|
|
||||||
req.onerror = (event) => {
|
req.onerror = (event) => {
|
||||||
|
|
@ -88,11 +85,40 @@ export class NodeStore {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
initializeSpecialNodes() {// {{{
|
initializeRootNode() {//{{{
|
||||||
this.nodes[ROOT_NODE] = new Node({ UUID: ROOT_NODE, Name: 'Start', Special: true }, -1)
|
return new Promise((resolve, reject) => {
|
||||||
this.nodes[DELETED_NODE] = new Node({ UUID: DELETED_NODE, Name: 'Deleted nodes', Special: true }, -1)
|
// The root node is a magical node which displays as the first node if none is specified.
|
||||||
this.nodes[ORPHANED_NODE] = new Node({ UUID: ORPHANED_NODE, Name: 'Orphaned nodes', Special: true }, -1)
|
// If not already existing, it will be created.
|
||||||
}// }}}
|
const trx = this.db.transaction('nodes', 'readwrite')
|
||||||
|
const nodes = trx.objectStore('nodes')
|
||||||
|
const getRequest = nodes.get(ROOT_NODE)
|
||||||
|
getRequest.onsuccess = (event) => {
|
||||||
|
// Root node exists - nice!
|
||||||
|
if (event.target.result !== undefined) {
|
||||||
|
resolve(event.target.result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const putRequest = nodes.put({
|
||||||
|
UUID: ROOT_NODE,
|
||||||
|
Name: 'Notes2',
|
||||||
|
Content: 'Hello, World!',
|
||||||
|
Updated: new Date().toISOString(),
|
||||||
|
ParentUUID: '',
|
||||||
|
})
|
||||||
|
putRequest.onsuccess = (event) => {
|
||||||
|
resolve(event.target.result)
|
||||||
|
}
|
||||||
|
putRequest.onerror = (event) => {
|
||||||
|
reject(event.target.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getRequest.onerror = (event) => reject(event.target.error)
|
||||||
|
})
|
||||||
|
}//}}}
|
||||||
|
purgeCache() {//{{{
|
||||||
|
this.nodes = {}
|
||||||
|
}//}}}
|
||||||
|
|
||||||
node(uuid, dataIfUndefined, newLevel) {//{{{
|
node(uuid, dataIfUndefined, newLevel) {//{{{
|
||||||
let n = this.nodes[uuid]
|
let n = this.nodes[uuid]
|
||||||
|
|
@ -150,30 +176,14 @@ export class NodeStore {
|
||||||
const nodeStore = trx.objectStore('nodes')
|
const nodeStore = trx.objectStore('nodes')
|
||||||
const index = nodeStore.index('byParent')
|
const index = nodeStore.index('byParent')
|
||||||
const req = index.getAll(storeParent)
|
const req = index.getAll(storeParent)
|
||||||
|
|
||||||
const hasChildrenPromises = []
|
|
||||||
req.onsuccess = (event) => {
|
req.onsuccess = (event) => {
|
||||||
const nodes = []
|
const nodes = []
|
||||||
for (const i in event.target.result) {
|
for (const i in event.target.result) {
|
||||||
const nodeData = event.target.result[i]
|
const nodeData = event.target.result[i]
|
||||||
const node = this.node(nodeData.UUID, nodeData, newLevel)
|
const node = this.node(nodeData.UUID, nodeData, newLevel)
|
||||||
|
|
||||||
// Look for the key of any children, a hopefully fast way
|
|
||||||
// to tell if any children exists at all and this node is a
|
|
||||||
// "folder". Needed quite early on for sorting.
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
|
||||||
const countReq = index.getKey(nodeData.UUID)
|
|
||||||
countReq.onsuccess = event => {
|
|
||||||
node.setHasChildren(event.target.result !== undefined)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
hasChildrenPromises.push(promise)
|
|
||||||
nodes.push(node)
|
nodes.push(node)
|
||||||
}
|
}
|
||||||
|
resolve(nodes)
|
||||||
Promise.all(hasChildrenPromises)
|
|
||||||
.then(() => resolve(nodes))
|
|
||||||
}
|
}
|
||||||
req.onerror = (event) => reject(event.target.error)
|
req.onerror = (event) => reject(event.target.error)
|
||||||
})
|
})
|
||||||
|
|
@ -221,7 +231,6 @@ export class NodeStore {
|
||||||
nodeStore = t.objectStore('nodes')
|
nodeStore = t.objectStore('nodes')
|
||||||
|
|
||||||
t.oncomplete = (_event) => {
|
t.oncomplete = (_event) => {
|
||||||
console.log('complete')
|
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,14 +255,6 @@ export class NodeStore {
|
||||||
}//}}}
|
}//}}}
|
||||||
get(uuid, suppliedNodestore) {//{{{
|
get(uuid, suppliedNodestore) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
switch (uuid) {
|
|
||||||
case ROOT_NODE:
|
|
||||||
case DELETED_NODE:
|
|
||||||
case ORPHANED_NODE:
|
|
||||||
resolve(this.nodes[uuid])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// A nodestore can be provided in order to
|
// A nodestore can be provided in order to
|
||||||
// avoid creating new transactions.
|
// avoid creating new transactions.
|
||||||
let trx
|
let trx
|
||||||
|
|
@ -291,16 +292,6 @@ export class NodeStore {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.UUID === DELETED_NODE || node.ParentUUID === DELETED_NODE) {
|
|
||||||
resolve(accumulated)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.UUID === ORPHANED_NODE || node.ParentUUID === ORPHANED_NODE) {
|
|
||||||
resolve(accumulated)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRequest = nodeParentIndex.get(node.ParentUUID)
|
const getRequest = nodeParentIndex.get(node.ParentUUID)
|
||||||
getRequest.onsuccess = (event) => {
|
getRequest.onsuccess = (event) => {
|
||||||
// Node not found in IndexedDB.
|
// Node not found in IndexedDB.
|
||||||
|
|
@ -351,7 +342,6 @@ class SimpleNodeStore {
|
||||||
// Node to be moved is first stored in the new queue.
|
// Node to be moved is first stored in the new queue.
|
||||||
const req = store.put(node.data)
|
const req = store.put(node.data)
|
||||||
req.onsuccess = () => {
|
req.onsuccess = () => {
|
||||||
console.log('here')
|
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
req.onerror = (event) => {
|
req.onerror = (event) => {
|
||||||
|
|
@ -429,91 +419,28 @@ class SimpleNodeStore {
|
||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeHistoryStore extends SimpleNodeStore {
|
export class StoreFile {
|
||||||
constructor(db, storeName) {//{{{
|
static createFromFileObject(f) {
|
||||||
super(db, storeName)
|
const obj = new StoreFile()
|
||||||
}//}}}
|
obj.name = f.name
|
||||||
count(uuid) {//{{{
|
obj.size = f.size
|
||||||
if (uuid === undefined)
|
obj.mime = f.type
|
||||||
return super.count()
|
return obj
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
this.name = ''
|
||||||
|
this.size = 0
|
||||||
|
this.mime = ''
|
||||||
|
|
||||||
const index = this.db
|
this.objectURL = null // URL.createObjectURL(blob)
|
||||||
.transaction(['nodes', this.storeName], 'readonly')
|
}
|
||||||
.objectStore(this.storeName)
|
data() {
|
||||||
.index('byUUID')
|
return {
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
}
|
||||||
const request = index.count(uuid)
|
|
||||||
request.onsuccess = (event) => resolve(event.target.result)
|
|
||||||
request.onerror = (event) => reject(event.target.error)
|
|
||||||
})
|
|
||||||
}//}}}
|
|
||||||
hasNode(uuid, updated) {// {{{
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const req = this.db
|
|
||||||
.transaction(['nodes', this.storeName], 'readonly')
|
|
||||||
.objectStore(this.storeName)
|
|
||||||
.getKey([uuid, updated])
|
|
||||||
|
|
||||||
req.onsuccess = (event) => {
|
|
||||||
resolve(event.target.result !== undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.onerror = (event) => {
|
|
||||||
console.log(event.target.error)
|
|
||||||
reject(event.target.error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}// }}}
|
|
||||||
retrievePage(uuid, perPage, page) {// {{{
|
|
||||||
return new Promise((resolve, _reject) => {
|
|
||||||
|
|
||||||
const lowerBound = [uuid, '00000000-0000-0000-0000-000000000000']
|
|
||||||
const upperBound = [uuid, 'ffffffff-ffff-ffff-ffff-ffffffffffff']
|
|
||||||
const range = IDBKeyRange.bound(lowerBound, upperBound)
|
|
||||||
|
|
||||||
const cursor = this.db
|
|
||||||
.transaction(['nodes', this.storeName], 'readonly')
|
|
||||||
.objectStore(this.storeName)
|
|
||||||
.openCursor(range, 'prev')
|
|
||||||
|
|
||||||
let retrieved = 0
|
|
||||||
let first = true
|
|
||||||
const nodes = []
|
|
||||||
|
|
||||||
cursor.onsuccess = (event) => {
|
|
||||||
const cursor = event.target.result
|
|
||||||
if (!cursor) {
|
|
||||||
resolve(nodes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// openCursor returns the first value which is only useful
|
|
||||||
// if the first page is requested.
|
|
||||||
if (page == 1 || !first) {
|
|
||||||
retrieved++
|
|
||||||
nodes.push(new Node(cursor.value))
|
|
||||||
if (retrieved === perPage) {
|
|
||||||
resolve(nodes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cursor.continue()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jump to the start of the requested page.
|
|
||||||
// Minus one since the first record was already returned.
|
|
||||||
if (page > 1 && first) {
|
|
||||||
first = false
|
|
||||||
cursor.advance((perPage * (page - 1)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}// }}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uuidv7() {// {{{
|
export function uuidv7() {
|
||||||
// random bytes
|
// random bytes
|
||||||
const value = new Uint8Array(16)
|
const value = new Uint8Array(16)
|
||||||
crypto.getRandomValues(value)
|
crypto.getRandomValues(value)
|
||||||
|
|
@ -537,6 +464,6 @@ export function uuidv7() {// {{{
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join("")
|
.join("")
|
||||||
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`
|
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`
|
||||||
}// }}}
|
}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
|
||||||
import { Node } from './page_node.mjs'
|
|
||||||
import { MarkedPosition } from './marked_position.mjs'
|
|
||||||
|
|
||||||
|
|
||||||
export class N2PageHistory extends CustomHTMLElement {
|
|
||||||
static PAGESIZE = 15
|
|
||||||
static {// {{{
|
|
||||||
this.tmpl = document.createElement('template')
|
|
||||||
this.tmpl.innerHTML = `
|
|
||||||
<style>
|
|
||||||
n2-pagehistory {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="back">
|
|
||||||
<img data-el="back-image" src="/images/${_VERSION}/icon_back.svg" class="colorize">
|
|
||||||
<div data-el="back-text">Back to node</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="node-name">
|
|
||||||
<img src="/images/${_VERSION}/icon_history.svg" class="colorize">
|
|
||||||
<h1 data-el="node-name"></h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column-1">
|
|
||||||
<div class="group-label">Actions</div>
|
|
||||||
<div class="group">
|
|
||||||
<button data-el="download-history">Fetch all history from server</button>
|
|
||||||
<div data-el="fetch-history-progress"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="group-label">History</div>
|
|
||||||
<div class="group">
|
|
||||||
<div data-el="stats">
|
|
||||||
<div>History on server:</div>
|
|
||||||
<div data-el="stats-on-server"></div>
|
|
||||||
|
|
||||||
<div>History on client:</div>
|
|
||||||
<div data-el="stats-on-client"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-el="nodes"></div>
|
|
||||||
|
|
||||||
<div data-el="pagination">
|
|
||||||
<div data-el="prev"><</div>
|
|
||||||
<div data-el="page"></div>
|
|
||||||
<div data-el="next">></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column-2">
|
|
||||||
<div class="group-label">Document</div>
|
|
||||||
<div class="group">
|
|
||||||
<div data-el="node-markdown"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
constructor() {// {{{
|
|
||||||
super()
|
|
||||||
this.selectedNode = null
|
|
||||||
|
|
||||||
this.setAttribute('tabindex', '-1')
|
|
||||||
this.addEventListener('keydown', event => this.keyHandler(event))
|
|
||||||
|
|
||||||
// Connect back icon and text to give the user a way back to the node.
|
|
||||||
this.elBackImage.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'node' }))
|
|
||||||
this.elBackText.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'node' }))
|
|
||||||
this.elPrev.addEventListener('click', () => this.prevPage())
|
|
||||||
this.elNext.addEventListener('click', () => this.nextPage())
|
|
||||||
this.elDownloadHistory.addEventListener('click', async () => {
|
|
||||||
await this.downloadHistory()
|
|
||||||
await this.useNode(this.node)
|
|
||||||
this.render(true)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
_mbus.subscribe('SHOW_PAGE', async (event) => {
|
|
||||||
if(event.detail.data.page != 'history')
|
|
||||||
return
|
|
||||||
|
|
||||||
await this.useNode(_app.nodeUI.node)
|
|
||||||
this.render()
|
|
||||||
})
|
|
||||||
|
|
||||||
_mbus.subscribe('HISTORY_NODE_SELECTED', (event) => {
|
|
||||||
this.selectedNode = event.detail.data.historyNode
|
|
||||||
|
|
||||||
// Any selected history node is rendered with markdown.
|
|
||||||
const marked = new MarkedPosition()
|
|
||||||
this.elNodeMarkdown.innerHTML = marked.parse(this.selectedNode?.node.content())
|
|
||||||
})
|
|
||||||
}// }}}
|
|
||||||
async render(keepFetchHistoryProgress) {// {{{
|
|
||||||
this.elNodeName.innerText = this.node.get('Name')
|
|
||||||
this.elPage.innerText = `${this.page} / ${this.pages}`
|
|
||||||
this.elStatsOnClient.innerText = `${this.nodesTotal}`
|
|
||||||
this.elStatsOnServer.innerText = `${this.historyOnServerTotal}`
|
|
||||||
|
|
||||||
if (this.nodesTotal <= N2PageHistory.PAGESIZE)
|
|
||||||
this.elPagination.style.display = 'none'
|
|
||||||
else
|
|
||||||
this.elPagination.style.display = ''
|
|
||||||
|
|
||||||
let nodes = await nodeStore.nodesHistory.retrievePage(this.node.UUID, N2PageHistory.PAGESIZE, this.page)
|
|
||||||
let i = 0
|
|
||||||
let divs = nodes.map(n => {
|
|
||||||
i++
|
|
||||||
const index = 1 + this.nodesTotal - (N2PageHistory.PAGESIZE * (this.page - 1) + i)
|
|
||||||
const div = new N2PageHistoryNode(n, index)
|
|
||||||
div.render()
|
|
||||||
return div
|
|
||||||
})
|
|
||||||
this.elNodes.replaceChildren(...divs)
|
|
||||||
|
|
||||||
if (!keepFetchHistoryProgress)
|
|
||||||
this.elFetchHistoryProgress.innerText = ''
|
|
||||||
|
|
||||||
// Select the first node.
|
|
||||||
if (!this.selectedNode) {
|
|
||||||
this.elNodes.firstElementChild?.select()
|
|
||||||
}
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
async useNode(node) {// {{{
|
|
||||||
this.node = node
|
|
||||||
this.page = 1
|
|
||||||
|
|
||||||
this.nodesTotal = await nodeStore.nodesHistory.count(this.node.UUID)
|
|
||||||
this.historyOnServerTotal = await this.getServerTotal()
|
|
||||||
this.pages = Math.ceil(this.nodesTotal / N2PageHistory.PAGESIZE)
|
|
||||||
}// }}}
|
|
||||||
keyHandler(event) {// {{{
|
|
||||||
let handled = true
|
|
||||||
switch (event.key) {
|
|
||||||
case 'ArrowLeft':
|
|
||||||
this.prevPage()
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'ArrowRight':
|
|
||||||
this.nextPage()
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'ArrowUp':
|
|
||||||
const prevNode = this.selectedNode?.previousElementSibling
|
|
||||||
if (prevNode)
|
|
||||||
prevNode.select()
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'ArrowDown':
|
|
||||||
const nextNode = this.selectedNode?.nextElementSibling
|
|
||||||
if (nextNode)
|
|
||||||
nextNode.select()
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
handled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handled) {
|
|
||||||
event.stopPropagation()
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
prevPage() {// {{{
|
|
||||||
if (this.page == 1)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Selecting a node on another page is wrong.
|
|
||||||
this.selectedNode = null
|
|
||||||
this.page--
|
|
||||||
this.render()
|
|
||||||
}// }}}
|
|
||||||
nextPage() {// {{{
|
|
||||||
if (this.page >= this.pages)
|
|
||||||
return
|
|
||||||
// Selecting a node on another page is wrong.
|
|
||||||
this.selectedNode = null
|
|
||||||
this.page++
|
|
||||||
this.render()
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
async getServerTotal() {// {{{
|
|
||||||
const res = await fetch(`/node/history/count/${this.node.UUID}`, {
|
|
||||||
headers: {
|
|
||||||
"Authorization": 'Bearer ' + localStorage.getItem('token'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
if (!json.OK) {
|
|
||||||
alert(json.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Count
|
|
||||||
}// }}}
|
|
||||||
async downloadHistory() {// {{{
|
|
||||||
try {
|
|
||||||
const nodes = []
|
|
||||||
let offset = 0
|
|
||||||
let hasMore = true
|
|
||||||
|
|
||||||
while (hasMore) {
|
|
||||||
const history = await this.downloadHistoryPage(offset)
|
|
||||||
hasMore = history.HasMore
|
|
||||||
for (const nodeData of history.Nodes) {
|
|
||||||
nodes.push(new Node(nodeData))
|
|
||||||
}
|
|
||||||
offset = nodes.length
|
|
||||||
this.elFetchHistoryProgress.innerText = `${nodes.length} fetched.`
|
|
||||||
}
|
|
||||||
|
|
||||||
let num = 0
|
|
||||||
for (const node of nodes) {
|
|
||||||
const ok = await nodeStore.nodesHistory.hasNode(node.UUID, node.get('Updated'))
|
|
||||||
if (ok) num++
|
|
||||||
await nodeStore.nodesHistory.add(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.elFetchHistoryProgress.innerText = `${nodes.length} fetched - all history fetched.`
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
alert(e)
|
|
||||||
}
|
|
||||||
}// }}}
|
|
||||||
async downloadHistoryPage(offset) {// {{{
|
|
||||||
const res = await fetch(`/node/history/retrieve/${this.node.UUID}/${offset}`, {
|
|
||||||
headers: {
|
|
||||||
"Authorization": 'Bearer ' + localStorage.getItem('token'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
if (!json.OK) {
|
|
||||||
alert(json.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return json
|
|
||||||
}// }}}
|
|
||||||
}
|
|
||||||
customElements.define('n2-pagehistory', N2PageHistory)
|
|
||||||
|
|
||||||
|
|
||||||
class N2PageHistoryNode extends CustomHTMLElement {
|
|
||||||
static {// {{{
|
|
||||||
this.tmpl = document.createElement('template')
|
|
||||||
this.tmpl.innerHTML = `
|
|
||||||
<div data-el="index"></div>
|
|
||||||
<div data-el="updated"><span data-el="date"></span> <span data-el="time"></span></div>
|
|
||||||
<div data-el="size"></div>
|
|
||||||
<div data-el="name"></div>
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
constructor(node, index) {// {{{
|
|
||||||
super()
|
|
||||||
|
|
||||||
this.node = node
|
|
||||||
this.index = index
|
|
||||||
|
|
||||||
this.style.display = 'contents'
|
|
||||||
this.selected = false
|
|
||||||
|
|
||||||
this.addEventListener('click', () => this.select())
|
|
||||||
|
|
||||||
// Another history node has been selected.
|
|
||||||
_mbus.subscribe('HISTORY_NODE_SELECTED', (event) => {
|
|
||||||
if (this.node.get('Updated') == event.detail.data.historyNode.node.get('Updated'))
|
|
||||||
return
|
|
||||||
this.selected = false
|
|
||||||
this.render()
|
|
||||||
})
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
select() {// {{{
|
|
||||||
this.selected = true
|
|
||||||
// Other nodes are told to unselect and rerender.
|
|
||||||
_mbus.dispatch('HISTORY_NODE_SELECTED', { historyNode: this })
|
|
||||||
this.render()
|
|
||||||
}// }}}
|
|
||||||
render() {// {{{
|
|
||||||
const date = this.node.get('Updated').slice(0, 10)
|
|
||||||
const time = this.node.get('Updated').slice(11, 19)
|
|
||||||
|
|
||||||
if (this.selected)
|
|
||||||
this.classList.add('selected')
|
|
||||||
else
|
|
||||||
this.classList.remove('selected')
|
|
||||||
|
|
||||||
this.elIndex.innerText = this.index
|
|
||||||
this.elDate.innerText = date
|
|
||||||
this.elTime.innerText = time
|
|
||||||
this.elSize.innerText = this.formatSize(this.node.get('Content').length)
|
|
||||||
this.elName.innerText = this.node.get('Name')
|
|
||||||
}// }}}
|
|
||||||
formatSize(s) {// {{{
|
|
||||||
let div = 1
|
|
||||||
let unit = 'B'
|
|
||||||
if (s >= 1048576) {
|
|
||||||
div = 1048576
|
|
||||||
unit = 'MB'
|
|
||||||
} else if (s >= 1024) {
|
|
||||||
div = 1024
|
|
||||||
unit = 'kB'
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Intl.NumberFormat(undefined, {
|
|
||||||
maximumFractionDigits: 0
|
|
||||||
}).format(Math.round(s / div)) + ' ' + unit
|
|
||||||
}// }}}
|
|
||||||
}
|
|
||||||
customElements.define('n2-pagehistorynode', N2PageHistoryNode)
|
|
||||||
|
|
@ -2,86 +2,21 @@ import { ROOT_NODE, uuidv7 } from 'node_store'
|
||||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||||
import { MarkedPosition } from './marked_position.mjs'
|
import { MarkedPosition } from './marked_position.mjs'
|
||||||
|
|
||||||
class N2NodeMenu extends CustomHTMLElement {
|
|
||||||
static {// {{{
|
|
||||||
this.tmpl = document.createElement('template')
|
|
||||||
this.tmpl.innerHTML = `
|
|
||||||
<style>
|
|
||||||
n2-nodemenu {
|
|
||||||
margin: 8px 0;
|
|
||||||
padding: 0;
|
|
||||||
position-anchor: --node-menu;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
|
|
||||||
|
|
||||||
top: anchor(bottom);
|
|
||||||
right: anchor(right);
|
|
||||||
left: auto;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
padding: 8px 16px 8px 8px;
|
|
||||||
border-bottom: 1px solid var(--line-color);
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: min-content 1fr;
|
|
||||||
grid-gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--menu-item-hover-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="node-menu">
|
|
||||||
<div class="menu-item" data-el="format-tables">
|
|
||||||
<img class="colorize" src="/images/${_VERSION}/icon_table.svg">
|
|
||||||
<div>Format tables</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="menu-item" data-el="history">
|
|
||||||
<img class="colorize" src="/images/${_VERSION}/icon_history.svg">
|
|
||||||
<div>History</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
constructor() {// {{{
|
|
||||||
super()
|
|
||||||
}// }}}
|
|
||||||
}
|
|
||||||
customElements.define('n2-nodemenu', N2NodeMenu)
|
|
||||||
|
|
||||||
export class N2PageNodeUI extends CustomHTMLElement {
|
export class N2PageNodeUI extends CustomHTMLElement {
|
||||||
static {// {{{
|
static {// {{{
|
||||||
this.tmpl = document.createElement('template')
|
this.tmpl = document.createElement('template')
|
||||||
this.tmpl.innerHTML = `
|
this.tmpl.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
n2-nodeui > .el-functions {
|
.el-functions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr repeat(3, min-content);
|
grid-template-columns: 1fr min-content min-content;
|
||||||
grid-gap: 8px;
|
grid-gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-items: end;
|
justify-items: end;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu {
|
|
||||||
anchor-name: --node-menu;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div data-el="name"></div>
|
<div data-el="name"></div>
|
||||||
|
|
@ -90,15 +25,10 @@ export class N2PageNodeUI extends CustomHTMLElement {
|
||||||
<div data-el="node-markdown" tabindex=1></div>
|
<div data-el="node-markdown" tabindex=1></div>
|
||||||
|
|
||||||
<div data-el="functions">
|
<div data-el="functions">
|
||||||
<img data-el="icon-save" src="/images/${_VERSION}/icon_save_disabled.svg">
|
|
||||||
<img data-el="icon-markdown">
|
<img data-el="icon-markdown">
|
||||||
<img data-el="icon-new-document" class="colorize" src="/images/${_VERSION}/icon_new_document.svg">
|
<img data-el="icon-save" src="/images/${_VERSION}/icon_save_disabled.svg">
|
||||||
<button data-el="menu" popovertarget="node-functions-menu">
|
<img data-el="icon-table-format" src="/images/${_VERSION}/icon_table.svg">
|
||||||
<img data-el="icon-menu" class="colorize" src="/images/${_VERSION}/icon_menu.svg">
|
|
||||||
</button>
|
|
||||||
<n2-nodemenu data-el="node-menu" id="node-functions-menu" popover></n2-nodemenu>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
`
|
`
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
|
|
@ -107,55 +37,45 @@ export class N2PageNodeUI extends CustomHTMLElement {
|
||||||
this.node = null
|
this.node = null
|
||||||
|
|
||||||
this.style.display = 'contents'
|
this.style.display = 'contents'
|
||||||
|
this.classList.add('show-markdown') // TODO Should probably be moved to settings.
|
||||||
this.marked = new MarkedPosition()
|
this.marked = new MarkedPosition()
|
||||||
|
|
||||||
_mbus.subscribe('NODE_UI_OPEN', event => {
|
_mbus.subscribe('NODE_UI_OPEN', event => {
|
||||||
this.node = event.detail.data
|
this.node = event.detail.data
|
||||||
|
this.showMarkdown(true)
|
||||||
|
|
||||||
if (!this.node.isSpecial())
|
|
||||||
this.showMarkdown(true)
|
|
||||||
this.render()
|
this.render()
|
||||||
})
|
})
|
||||||
|
|
||||||
_mbus.subscribe('NODE_MODIFIED', () => {
|
_mbus.subscribe('NODE_MODIFIED', () => {
|
||||||
this.classList.add('node-modified')
|
document.querySelector('#crumbs .crumbs')?.classList.add('node-modified')
|
||||||
this.elIconSave.src = `/images/${_VERSION}/icon_save.svg`
|
this.elIconSave.src = `/images/${_VERSION}/icon_save.svg`
|
||||||
this.elIconSave.classList.add('colorize')
|
|
||||||
this.renderName()
|
this.renderName()
|
||||||
})
|
})
|
||||||
|
|
||||||
_mbus.subscribe('NODE_UNMODIFIED', () => {
|
_mbus.subscribe('NODE_UNMODIFIED', () => {
|
||||||
this.classList.remove('node-modified')
|
document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified')
|
||||||
this.elIconSave.src = `/images/${_VERSION}/icon_save_disabled.svg`
|
this.elIconSave.src = `/images/${_VERSION}/icon_save_disabled.svg`
|
||||||
this.elIconSave.classList.remove('colorize')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
_mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown()))
|
_mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown()))
|
||||||
_mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data))
|
_mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data))
|
||||||
_mbus.subscribe('MARKDOWN_CHANGE_CHECKBOX', ({ detail }) => this.checkboxUpdated(detail.data))
|
|
||||||
|
|
||||||
// Binding the node rename handler.
|
this.elName.addEventListener('click', () => {
|
||||||
this.elName.addEventListener('click', async () => this.renameNode())
|
const name = prompt('Change title', this.node.data.Name)
|
||||||
|
if (name === null)
|
||||||
|
return
|
||||||
|
|
||||||
// Bind handlers for content keyboard input and paste.
|
try {
|
||||||
|
this.node.setName(name)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
|
this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
|
||||||
this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event))
|
this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event))
|
||||||
|
|
||||||
// Bind node icon handlers.
|
|
||||||
this.elIconSave.addEventListener('click', () => this.saveNode())
|
|
||||||
this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown()))
|
this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown()))
|
||||||
this.elIconNewDocument.addEventListener('click', event => {
|
this.elIconTableFormat.addEventListener('click', event => {
|
||||||
if (event.shiftKey)
|
|
||||||
_app.createNode(this.node.ParentUUID)
|
|
||||||
else
|
|
||||||
_app.createNode()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Bind node menu items to handlers.
|
|
||||||
this.elNodeMenu.elFormatTables.addEventListener('click', event => {
|
|
||||||
this.elNodeMenu.hidePopover()
|
|
||||||
|
|
||||||
if (!event.shiftKey)
|
if (!event.shiftKey)
|
||||||
this.elNodeContent.value = this.formatAllTables(this.elNodeContent.value)
|
this.elNodeContent.value = this.formatAllTables(this.elNodeContent.value)
|
||||||
else {
|
else {
|
||||||
|
|
@ -169,12 +89,7 @@ export class N2PageNodeUI extends CustomHTMLElement {
|
||||||
|
|
||||||
this.node.setContent(this.elNodeContent.value)
|
this.node.setContent(this.elNodeContent.value)
|
||||||
})
|
})
|
||||||
this.elNodeMenu.elHistory.addEventListener('click', () => {
|
|
||||||
_mbus.dispatch('SHOW_PAGE', { page: 'history' })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Default is to always show markdown.
|
|
||||||
this.classList.add('show-markdown') // TODO Should probably be moved to settings.
|
|
||||||
this.showMarkdown(true)
|
this.showMarkdown(true)
|
||||||
}// }}}
|
}// }}}
|
||||||
renderName() {// {{{
|
renderName() {// {{{
|
||||||
|
|
@ -187,41 +102,9 @@ export class N2PageNodeUI extends CustomHTMLElement {
|
||||||
}// }}}
|
}// }}}
|
||||||
takeFocus() {// {{{
|
takeFocus() {// {{{
|
||||||
if (this.showMarkdown()) {
|
if (this.showMarkdown()) {
|
||||||
this.elNodeMarkdown.focus({ preventScroll: true })
|
this.elNodeMarkdown.focus()
|
||||||
} else
|
} else
|
||||||
this.elNodeContent.focus({ preventScroll: true })
|
this.elNodeContent.focus()
|
||||||
}// }}}
|
|
||||||
async renameNode() {// {{{
|
|
||||||
const name = prompt('Change title', this.node.data.Name)
|
|
||||||
if (name === null)
|
|
||||||
return
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Document isn't only renamed, but also saved at once.
|
|
||||||
// Not really correct, but good enough to not have to implement
|
|
||||||
// a separate way to only rename the document. Since history is
|
|
||||||
// preserved it shouldn't be that horrible.
|
|
||||||
this.node.setName(name)
|
|
||||||
await this.node.save()
|
|
||||||
|
|
||||||
// Re-render the parent treenode forcefully to sort it again.
|
|
||||||
const parentUUID = this.node.ParentUUID
|
|
||||||
if (!parentUUID)
|
|
||||||
return
|
|
||||||
const parentTreeNode = _app.sidebar.getTreeNode(parentUUID)
|
|
||||||
parentTreeNode?.render(true, true)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
alert(err)
|
|
||||||
}
|
|
||||||
}// }}}
|
|
||||||
async saveNode() {// {{{
|
|
||||||
if (!this.node.isModified())
|
|
||||||
return
|
|
||||||
|
|
||||||
// node.save takes care of both "nodes" and "nodes_history" stores, also adds it to send queue.
|
|
||||||
// Sets "Updated" value to current date and time and generates a new history UUID.
|
|
||||||
await this.node.save()
|
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
contentChanged(event) {//{{{
|
contentChanged(event) {//{{{
|
||||||
|
|
@ -240,12 +123,10 @@ export class N2PageNodeUI extends CustomHTMLElement {
|
||||||
case true:
|
case true:
|
||||||
this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value)
|
this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value)
|
||||||
this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown.svg`
|
this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown.svg`
|
||||||
this.elIconMarkdown.classList.add('colorize')
|
|
||||||
this.classList.add('show-markdown')
|
this.classList.add('show-markdown')
|
||||||
break
|
break
|
||||||
case false:
|
case false:
|
||||||
this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown_hollow.svg`
|
this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown_hollow.svg`
|
||||||
this.elIconMarkdown.classList.remove('colorize')
|
|
||||||
this.classList.remove('show-markdown')
|
this.classList.remove('show-markdown')
|
||||||
break
|
break
|
||||||
case null:
|
case null:
|
||||||
|
|
@ -371,61 +252,25 @@ export class N2PageNodeUI extends CustomHTMLElement {
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
}// }}}
|
}// }}}
|
||||||
// "marked" sends a messagebus event when checking/unchecking a checkbox.
|
|
||||||
// Updates node and content textarea.
|
|
||||||
checkboxUpdated(eventData) {// {{{
|
|
||||||
const checkbox = eventData.checkbox
|
|
||||||
const pos = eventData.position
|
|
||||||
const content = this.node.content()
|
|
||||||
|
|
||||||
// Basic validation to verify that Marked does what is known and expected at this writing.
|
|
||||||
const mdCheckboxStr = content.slice(pos.start, pos.end)
|
|
||||||
if (!mdCheckboxStr.match(/^\[[ xX]\] $/)) {
|
|
||||||
alert(`Checkbox string didn't pass validation: '${mdCheckboxStr}'`)
|
|
||||||
console.error(`Checkbox string didn't pass validation: '${mdCheckboxStr}'`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node is modified with the new value. User has to save manually, otherwise other changes could be saved
|
|
||||||
// when a save wasn't expected.
|
|
||||||
const newValue = `[${checkbox.checked ? 'x' : ' '}] `
|
|
||||||
const modifiedContent = this.node.content().slice(0, pos.start) + newValue + this.node.content().slice(pos.end)
|
|
||||||
this.node.setContent(modifiedContent)
|
|
||||||
|
|
||||||
// Also update the textarea since the node model doesn't know about it.
|
|
||||||
this.elNodeContent.setRangeText(newValue, pos.start, pos.end, 'select')
|
|
||||||
|
|
||||||
}// }}}
|
|
||||||
}
|
}
|
||||||
customElements.define('n2-nodeui', N2PageNodeUI)
|
customElements.define('n2-nodeui', N2PageNodeUI)
|
||||||
|
|
||||||
export class Node {
|
export class Node {
|
||||||
static sort(a, b) {//{{{
|
static sort(a, b) {//{{{
|
||||||
// Nodes with children ("folders") are sorted first.
|
if (a.data.Name < b.data.Name) return -1
|
||||||
if (a._has_children && !b._has_children) return -1
|
if (a.data.Name > b.data.Name) return 0
|
||||||
if (!a._has_children && b._has_children) return 1
|
|
||||||
|
|
||||||
// Otherwise sort by lowercased name.
|
|
||||||
const an = a.data.Name.toLowerCase()
|
|
||||||
const bn = b.data.Name.toLowerCase()
|
|
||||||
if (an < bn) return -1
|
|
||||||
if (an > bn) return 1
|
|
||||||
return 0
|
return 0
|
||||||
}//}}}
|
}//}}}
|
||||||
static create(name, parentUUID) {// {{{
|
static create(name, parentUUID) {// {{{
|
||||||
const node = new Node({
|
return new Node({
|
||||||
UUID: uuidv7(),
|
UUID: uuidv7(),
|
||||||
Created: (new Date()).toISOString(),
|
Created: (new Date()).toISOString(),
|
||||||
Content: '',
|
Content: '',
|
||||||
Name: name,
|
Name: name,
|
||||||
ParentUUID: parentUUID,
|
ParentUUID: parentUUID,
|
||||||
Markdown: false,
|
Markdown: false,
|
||||||
|
History: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Newly created node (not constructed from existing data) is considered modified
|
|
||||||
// since node.save returns early if it isn't modified.
|
|
||||||
node._modified = true
|
|
||||||
|
|
||||||
return node
|
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
constructor(nodeData, level) {//{{{
|
constructor(nodeData, level) {//{{{
|
||||||
|
|
@ -441,7 +286,6 @@ export class Node {
|
||||||
this.ParentUUID = nodeData.ParentUUID
|
this.ParentUUID = nodeData.ParentUUID
|
||||||
|
|
||||||
this._children_fetched = false
|
this._children_fetched = false
|
||||||
this._has_children = null // this will be set by nodeStore.getTreeNodes
|
|
||||||
this.Children = []
|
this.Children = []
|
||||||
this.Ancestors = []
|
this.Ancestors = []
|
||||||
|
|
||||||
|
|
@ -478,7 +322,6 @@ export class Node {
|
||||||
this.Children.sort(Node.sort)
|
this.Children.sort(Node.sort)
|
||||||
|
|
||||||
const numChildren = this.Children.length
|
const numChildren = this.Children.length
|
||||||
this.setHasChildren(numChildren > 0)
|
|
||||||
for (let i = 0; i < numChildren; i++) {
|
for (let i = 0; i < numChildren; i++) {
|
||||||
if (i > 0)
|
if (i > 0)
|
||||||
this.Children[i]._sibling_before = this.Children[i - 1]
|
this.Children[i]._sibling_before = this.Children[i - 1]
|
||||||
|
|
@ -487,13 +330,14 @@ export class Node {
|
||||||
this.Children[i]._parent = this
|
this.Children[i]._parent = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify the tree that all children are fetched and ready to process.
|
||||||
|
//_notes2.current.tree.fetchChildrenOn(this.UUID)
|
||||||
|
_mbus.dispatch(`NODE_CHILDREN_FETCHED_${this.UUID}`)
|
||||||
|
|
||||||
return this.Children
|
return this.Children
|
||||||
}//}}}
|
}//}}}
|
||||||
setHasChildren(v) {// {{{
|
|
||||||
this._has_children = v
|
|
||||||
}// }}}
|
|
||||||
hasChildren() {//{{{
|
hasChildren() {//{{{
|
||||||
return this._has_children
|
return this.Children.length > 0
|
||||||
}//}}}
|
}//}}}
|
||||||
getSiblingBefore() {// {{{
|
getSiblingBefore() {// {{{
|
||||||
return this._sibling_before
|
return this._sibling_before
|
||||||
|
|
@ -504,23 +348,12 @@ export class Node {
|
||||||
getParent() {//{{{
|
getParent() {//{{{
|
||||||
return this._parent
|
return this._parent
|
||||||
}//}}}
|
}//}}}
|
||||||
moveToParent(newParentUUID) {// {{{
|
|
||||||
if (this.UUID === newParentUUID)
|
|
||||||
throw new Error("New parent UUID is the same as node UUID. Can't be your own parent.")
|
|
||||||
|
|
||||||
this.ParentUUID = newParentUUID
|
|
||||||
this.data.ParentUUID = newParentUUID
|
|
||||||
this._modified = true
|
|
||||||
}// }}}
|
|
||||||
isLastSibling() {//{{{
|
isLastSibling() {//{{{
|
||||||
return this._sibling_after === null
|
return this._sibling_after === null
|
||||||
}//}}}
|
}//}}}
|
||||||
isFirstSibling() {//{{{
|
isFirstSibling() {//{{{
|
||||||
return this._sibling_before === null
|
return this._sibling_before === null
|
||||||
}//}}}
|
}//}}}
|
||||||
isSpecial() {// {{{
|
|
||||||
return this.data.Special
|
|
||||||
}// }}}
|
|
||||||
content() {//{{{
|
content() {//{{{
|
||||||
/* TODO - implement crypto
|
/* TODO - implement crypto
|
||||||
if (this.CryptoKeyID != 0 && !this._decrypted)
|
if (this.CryptoKeyID != 0 && !this._decrypted)
|
||||||
|
|
@ -542,52 +375,17 @@ export class Node {
|
||||||
_mbus.dispatch('NODE_MODIFIED', { node: this })
|
_mbus.dispatch('NODE_MODIFIED', { node: this })
|
||||||
}// }}}
|
}// }}}
|
||||||
async save() {//{{{
|
async save() {//{{{
|
||||||
// Just safeguarding not using the root node,
|
|
||||||
// which sort of exist but isn't supposed to communicate to server.
|
|
||||||
if (this.UUID == ROOT_NODE)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.data.Content = this._content
|
this.data.Content = this._content
|
||||||
this.data.Updated = new Date().toISOString()
|
this.data.Updated = new Date().toISOString()
|
||||||
this.data.HistoryUUID = uuidv7() // every time the node is saved a new history UUID identifies the changed node.
|
|
||||||
this._modified = false
|
this._modified = false
|
||||||
|
|
||||||
_mbus.dispatch('NODE_UNMODIFIED')
|
_mbus.dispatch('NODE_UNMODIFIED')
|
||||||
|
|
||||||
// When stored into database and ancestry was changed,
|
// When stored into database and ancestry was changed,
|
||||||
// the ancestry path could be interesting.
|
// the ancestry path could be interesting.
|
||||||
/*
|
|
||||||
const ancestors = await nodeStore.getNodeAncestry(this)
|
const ancestors = await nodeStore.getNodeAncestry(this)
|
||||||
this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse()
|
this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse()
|
||||||
*/
|
|
||||||
/* The node history is a local store for node history.
|
|
||||||
* This could be provisioned from the server or cleared if
|
|
||||||
* deemed unnecessary.
|
|
||||||
*
|
|
||||||
* The send queue is what will be sent back to the server
|
|
||||||
* to have a recorded history of the notes.
|
|
||||||
*
|
|
||||||
* A setting to be implemented in the future could be to
|
|
||||||
* not save the history locally at all. */
|
|
||||||
|
|
||||||
// Current node is added to history. It will be duplicated with the "nodes" store
|
|
||||||
// for simplicity, to hopefully avoid bugs.
|
|
||||||
const history = nodeStore.nodesHistory.add(this)
|
|
||||||
|
|
||||||
// Updated node is added to the send queue to be stored on server.
|
|
||||||
|
|
||||||
const sendQueue = nodeStore.sendQueue.add(this)
|
|
||||||
|
|
||||||
// Updated node is saved to the primary node store.
|
|
||||||
const nodeStoreAdding = nodeStore.add([this])
|
|
||||||
|
|
||||||
console.log('waiting')
|
|
||||||
await Promise.all([history, sendQueue, nodeStoreAdding])
|
|
||||||
console.log('waiting done')
|
|
||||||
|
|
||||||
return
|
|
||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -1,771 +0,0 @@
|
||||||
import { ROOT_NODE, ORPHANED_NODE, DELETED_NODE } from 'node_store'
|
|
||||||
import { Node } from 'node'
|
|
||||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
|
||||||
import { Color, Solver } from './lib/css_colorize.mjs'
|
|
||||||
|
|
||||||
// TreeExpandedHandler is responsible for collapsing or expanding
|
|
||||||
// the node tree, wide view or narrow "mobile" view.
|
|
||||||
class TreeExpansionHandler {// {{{
|
|
||||||
constructor() {
|
|
||||||
this.isNarrow = false
|
|
||||||
this.initializeMediaHandler()
|
|
||||||
this.initializeBusEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeBusEvents() {
|
|
||||||
_mbus.subscribe('TREE_EXPANSION', ({ detail }) => {
|
|
||||||
// When a node is selected on the screen and the screen
|
|
||||||
// is narrow the tree is automatically hidden.
|
|
||||||
//
|
|
||||||
// Can't always hide the tree automatically when a node
|
|
||||||
// is selected since the wide mode shows the tree as standard.
|
|
||||||
if (detail.data?.when == 'narrow' && !this.isNarrow)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.treeExpansion(detail.data?.expand)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeMediaHandler() {
|
|
||||||
const query = window.matchMedia('(max-width: 800px)')
|
|
||||||
query.addEventListener('change', event => this.screenNarrowHandler(event))
|
|
||||||
|
|
||||||
// Run once to set initial state, instead of needing to toggle state.
|
|
||||||
this.screenNarrowHandler(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When screen becomes narrow, the tree is automatically hidden.
|
|
||||||
// Primary purpose is to read content, not browse, which is why
|
|
||||||
// the tree is hidden as standard.
|
|
||||||
screenNarrowHandler(event) {
|
|
||||||
this.isNarrow = event.matches
|
|
||||||
|
|
||||||
if (this.isNarrow)
|
|
||||||
this.treeExpansion(false)
|
|
||||||
else
|
|
||||||
this.treeExpansion(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
treeExpansion(expanded) {
|
|
||||||
const notes2 = document.getElementById('notes2')
|
|
||||||
|
|
||||||
if (expanded) {
|
|
||||||
notes2.classList.remove('hide-tree')
|
|
||||||
notes2.classList.add('show-tree')
|
|
||||||
} else {
|
|
||||||
notes2.classList.add('hide-tree')
|
|
||||||
notes2.classList.remove('show-tree')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
export class N2Sidebar extends CustomHTMLElement {
|
|
||||||
static {// {{{
|
|
||||||
this.tmpl = document.createElement('template')
|
|
||||||
this.tmpl.innerHTML = `
|
|
||||||
<style>
|
|
||||||
n2-sidebar {
|
|
||||||
#logo {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr min-content;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: start;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 16px;
|
|
||||||
border-bottom: 1px solid var(--line-color);
|
|
||||||
|
|
||||||
img:first-child {
|
|
||||||
height: 24px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 16px;
|
|
||||||
gap: 8px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid var(--line-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.el-hide-tree {
|
|
||||||
font-size: 1.25em;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div id="logo">
|
|
||||||
<img data-el="logo" src="/images/${_VERSION}/logo.svg" />
|
|
||||||
<div data-el="hide-tree"><</div>
|
|
||||||
</div>
|
|
||||||
<div class="icons">
|
|
||||||
<img data-el="sync" class='sync colorize' src="/images/${_VERSION}/icon_refresh.svg" />
|
|
||||||
<img data-el="search" class='search colorize' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
|
|
||||||
<img data-el="settings" class='settings colorize' src="/images/${_VERSION}/icon_settings.svg" />
|
|
||||||
</div>
|
|
||||||
<div data-el="treenodes"></div>
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
constructor() {// {{{
|
|
||||||
super()
|
|
||||||
|
|
||||||
this.id = 'tree-nodes'
|
|
||||||
this.tabIndex = 0
|
|
||||||
|
|
||||||
this.treeNodeComponents = {}
|
|
||||||
this.expandedNodes = {} // keyed on UUID
|
|
||||||
this.selectedNode = null
|
|
||||||
this.rendered = false
|
|
||||||
|
|
||||||
new TreeExpansionHandler()
|
|
||||||
|
|
||||||
this.addEventListener('keydown', event => this.keyHandler(event))
|
|
||||||
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 })
|
|
||||||
})
|
|
||||||
|
|
||||||
_mbus.subscribe('NODE_MODIFIED', ({ detail }) => {
|
|
||||||
const node = detail.data.node
|
|
||||||
const treenode = this.treeNodeComponents[node.get('UUID')]
|
|
||||||
|
|
||||||
if (!treenode)
|
|
||||||
return
|
|
||||||
|
|
||||||
treenode.node = node
|
|
||||||
treenode.render(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
/* XXX - set color */
|
|
||||||
let color = new Color(0x80, 0x00, 0x33)
|
|
||||||
let solver = new Solver(color)
|
|
||||||
let result = solver.solve()
|
|
||||||
console.log(result.filter)
|
|
||||||
}// }}}
|
|
||||||
async render() {// {{{
|
|
||||||
if (this.rendered)
|
|
||||||
alert('Tree should only be rendered once.')
|
|
||||||
|
|
||||||
this.expandedNodes[ROOT_NODE] = true
|
|
||||||
const startnode = await nodeStore.get(ROOT_NODE)
|
|
||||||
const starttreenode = new N2TreeNode(this, startnode, null)
|
|
||||||
|
|
||||||
const deletednode = await nodeStore.get(DELETED_NODE)
|
|
||||||
const deletedtreenode = new SpecialNodeDeleted(this, deletednode, null)
|
|
||||||
|
|
||||||
const orphanednode = await nodeStore.get(ORPHANED_NODE)
|
|
||||||
const orphanedtreenode = new SpecialNodeOrphaned(this, orphanednode, null)
|
|
||||||
|
|
||||||
startnode._sibling_after = deletednode
|
|
||||||
deletednode._sibling_before = startnode
|
|
||||||
|
|
||||||
deletednode._sibling_after = orphanednode
|
|
||||||
orphanednode._sibling_before = deletednode
|
|
||||||
|
|
||||||
this.treeNodeComponents[startnode.UUID] = starttreenode
|
|
||||||
this.treeNodeComponents[deletednode.UUID] = deletedtreenode
|
|
||||||
this.treeNodeComponents[orphanednode.UUID] = orphanedtreenode
|
|
||||||
|
|
||||||
this.elTreenodes.appendChild(await starttreenode.render())
|
|
||||||
this.elTreenodes.appendChild(await deletedtreenode.render())
|
|
||||||
this.elTreenodes.appendChild(await orphanedtreenode.render())
|
|
||||||
|
|
||||||
// Notify the application that the initial tree is rendered (with children)
|
|
||||||
// and that initial node selection can take place. App will check URL to
|
|
||||||
// select the correct one.
|
|
||||||
_mbus.dispatch('TREE_RENDERED')
|
|
||||||
|
|
||||||
this.rendered = true
|
|
||||||
return this
|
|
||||||
}// }}}
|
|
||||||
reset() {// {{{
|
|
||||||
this.treeNodeComponents = {}
|
|
||||||
this.rendered = false
|
|
||||||
this.elTreenodes.replaceChildren()
|
|
||||||
this.render()
|
|
||||||
}// }}}
|
|
||||||
getNodeExpanded(UUID) {//{{{
|
|
||||||
if (this.expandedNodes[UUID] === undefined)
|
|
||||||
this.expandedNodes[UUID] = false
|
|
||||||
return this.expandedNodes[UUID]
|
|
||||||
}//}}}
|
|
||||||
async setNodeExpanded(node, value) {//{{{
|
|
||||||
let expanded = this.expandedNodes[node.UUID]
|
|
||||||
if (expanded === undefined) {
|
|
||||||
this.expandedNodes[node.UUID] = false
|
|
||||||
expanded = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expanded === value)
|
|
||||||
return
|
|
||||||
|
|
||||||
this.expandedNodes[node.UUID] = value
|
|
||||||
_mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value)
|
|
||||||
}//}}}
|
|
||||||
setSelected(node, dontExpand) {//{{{
|
|
||||||
if (node === undefined)
|
|
||||||
return
|
|
||||||
|
|
||||||
// The previously selected node, if any, needs to be rerendered
|
|
||||||
// to not retain its 'selected' class.
|
|
||||||
const prevUUID = this.selectedNode?.UUID
|
|
||||||
this.selectedNode = node
|
|
||||||
if (prevUUID)
|
|
||||||
this.treeNodeComponents[prevUUID]?.render(true)
|
|
||||||
|
|
||||||
// And now the newly selected node is rerendered.
|
|
||||||
this.treeNodeComponents[node.UUID]?.render(true)
|
|
||||||
|
|
||||||
if (!dontExpand)
|
|
||||||
this.setNodeExpanded(node, true)
|
|
||||||
}//}}}
|
|
||||||
isSelected(node) {//{{{
|
|
||||||
return this.selectedNode?.UUID === node.UUID
|
|
||||||
}//}}}
|
|
||||||
getTreeNode(uuid) {// {{{
|
|
||||||
return this.treeNodeComponents[uuid]
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
async keyHandler(event) {//{{{
|
|
||||||
let handled = true
|
|
||||||
const n = this.selectedNode
|
|
||||||
const Space = ' '
|
|
||||||
|
|
||||||
// This handler would otherwise react to stuff like Ctrl+L.
|
|
||||||
if (event.ctrlKey || event.altKey)
|
|
||||||
return
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
// Space and enter is toggling expansion.
|
|
||||||
// Holding shift down does it recursively.
|
|
||||||
case Space:
|
|
||||||
case 'Enter':
|
|
||||||
const expanded = this.getNodeExpanded(n.UUID)
|
|
||||||
if (event.shiftKey) {
|
|
||||||
this.recursiveExpand(n, !expanded)
|
|
||||||
} else {
|
|
||||||
this.setNodeExpanded(n, !expanded)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'Home':
|
|
||||||
this.navigateTop()
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'End':
|
|
||||||
this.navigateBottom()
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'ArrowDown':
|
|
||||||
await this.navigateDown(this.selectedNode)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'ArrowUp':
|
|
||||||
await this.navigateUp(this.selectedNode)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'ArrowLeft':
|
|
||||||
await this.navigateLeft(this.selectedNode)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'ArrowRight':
|
|
||||||
await this.navigateRight(this.selectedNode)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
handled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handled) {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
}
|
|
||||||
}//}}}
|
|
||||||
async navigateLeft(n) {//{{{
|
|
||||||
if (n === null || n === undefined || n.UUID == ROOT_NODE)
|
|
||||||
return
|
|
||||||
|
|
||||||
const expanded = this.getNodeExpanded(n.UUID)
|
|
||||||
if (expanded && n.hasChildren() && n.UUID !== ROOT_NODE) {
|
|
||||||
this.setNodeExpanded(n, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n.isFirstSibling()) {
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const siblingBefore = n.getSiblingBefore()
|
|
||||||
const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
|
|
||||||
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
|
||||||
const siblingAbove = this.getLastExpandedNode(siblingBefore)
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
}//}}}
|
|
||||||
async navigateRight(n) {//{{{
|
|
||||||
if (n === null || n === undefined)
|
|
||||||
return
|
|
||||||
|
|
||||||
const siblingAfter = n.getSiblingAfter()
|
|
||||||
const expanded = this.getNodeExpanded(n.UUID)
|
|
||||||
|
|
||||||
if (!expanded && n.hasChildren()) {
|
|
||||||
this.setNodeExpanded(n, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expanded && n.hasChildren()) {
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n.isLastSibling()) {
|
|
||||||
const nextNode = this.getParentWithNextSibling(n)
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
}//}}}
|
|
||||||
async navigateUp(n) {//{{{
|
|
||||||
if (n === null || n === undefined || n.UUID == ROOT_NODE)
|
|
||||||
return
|
|
||||||
|
|
||||||
let parent = null
|
|
||||||
const siblingBefore = n.getSiblingBefore()
|
|
||||||
let siblingExpanded = false
|
|
||||||
if (siblingBefore !== null)
|
|
||||||
siblingExpanded = this.getNodeExpanded(siblingBefore.UUID)
|
|
||||||
|
|
||||||
if (n.isFirstSibling()) {
|
|
||||||
parent = n.getParent()
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
|
||||||
const nodeVisuallyAbove = this.getLastExpandedNode(siblingBefore)
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: nodeVisuallyAbove.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siblingBefore) {
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}//}}}
|
|
||||||
async navigateDown(n) {//{{{
|
|
||||||
if (n === null || n === undefined)
|
|
||||||
return
|
|
||||||
|
|
||||||
const nodeExpanded = this.getNodeExpanded(n.UUID)
|
|
||||||
|
|
||||||
// Last node, not expanded, so it matters not whether it has children or not.
|
|
||||||
// Traverse upward to nearest parent with next sibling.
|
|
||||||
if (!nodeExpanded && n.isLastSibling()) {
|
|
||||||
const wantedNode = this.getParentWithNextSibling(n)
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
|
|
||||||
const wantedNode = this.getParentWithNextSibling(n)
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node not expanded. Go to this node's next sibling.
|
|
||||||
// GoToNode will abort if given null.
|
|
||||||
if (!nodeExpanded || !n.hasChildren()) {
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node is expanded.
|
|
||||||
// Children will be visually beneath this node, if any.
|
|
||||||
if (nodeExpanded && n.hasChildren()) {
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: false, dontExpand: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}//}}}
|
|
||||||
async navigateTop() {//{{{
|
|
||||||
const root = await nodeStore.get(ROOT_NODE)
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: root.UUID, dontPush: false, dontExpand: true })
|
|
||||||
}//}}}
|
|
||||||
async navigateBottom() {//{{{
|
|
||||||
const orphaned = await nodeStore.get(ORPHANED_NODE)
|
|
||||||
|
|
||||||
if (!orphaned.hasChildren() || this.getNodeExpanded(orphaned.UUID)) {
|
|
||||||
_mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.UUID, dontPush: false, dontExpand: true })
|
|
||||||
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: orphaned.Children[orphaned.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
|
|
||||||
*/
|
|
||||||
}//}}}
|
|
||||||
|
|
||||||
getParentWithNextSibling(node) {//{{{
|
|
||||||
let currNode = node
|
|
||||||
while (currNode !== null && currNode.UUID !== ROOT_NODE && currNode.getSiblingAfter() === null) {
|
|
||||||
currNode = currNode.getParent()
|
|
||||||
}
|
|
||||||
return currNode?.getSiblingAfter()
|
|
||||||
}//}}}
|
|
||||||
getLastExpandedNode(node) {//{{{
|
|
||||||
let currNode = node
|
|
||||||
while (this.getNodeExpanded(currNode.UUID) && currNode.hasChildren()) {
|
|
||||||
currNode = currNode.Children[currNode.Children.length - 1]
|
|
||||||
}
|
|
||||||
return currNode
|
|
||||||
}//}}}
|
|
||||||
async recursiveExpand(node, state) {//{{{
|
|
||||||
if (state)
|
|
||||||
await this.setNodeExpanded(node, true)
|
|
||||||
|
|
||||||
// An expanded node needs to have its children fetched.
|
|
||||||
if (!node.hasFetchedChildren())
|
|
||||||
await node.fetchChildren()
|
|
||||||
|
|
||||||
for (const child of node.Children)
|
|
||||||
await this.recursiveExpand(child, state)
|
|
||||||
|
|
||||||
if (!state)
|
|
||||||
await this.setNodeExpanded(node, false)
|
|
||||||
}//}}}
|
|
||||||
async makeVisible(node, providedAncestors, dontExpand) {// {{{
|
|
||||||
const treenode = this.treeNodeComponents[node.UUID]
|
|
||||||
|
|
||||||
if (!dontExpand) {
|
|
||||||
const ancestors = providedAncestors || await nodeStore.getNodeAncestry(node)
|
|
||||||
for (const ancestor of ancestors.reverse()) {
|
|
||||||
this.setNodeExpanded(ancestor, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
treenode?.scrollIntoView({ block: 'nearest' })
|
|
||||||
}// }}}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class N2TreeNode extends CustomHTMLElement {
|
|
||||||
static DRAG_ICON = new Image()
|
|
||||||
static DRAG_ICON_OK = new Image()
|
|
||||||
|
|
||||||
static {// {{{
|
|
||||||
N2TreeNode.DRAG_ICON.src = `/images/${_VERSION}/leaf.svg`
|
|
||||||
N2TreeNode.DRAG_ICON_OK.src = `/images/${_VERSION}/expanded.svg`
|
|
||||||
|
|
||||||
this.tmpl = document.createElement('template')
|
|
||||||
this.tmpl.innerHTML = `
|
|
||||||
<style>
|
|
||||||
n2-sidebar:focus-within {
|
|
||||||
n2-specialnodedeleted > .el-name,
|
|
||||||
n2-specialnodeorphaned > .el-name,
|
|
||||||
n2-treenode > .el-name {
|
|
||||||
&.selected {
|
|
||||||
span {
|
|
||||||
position:relative;
|
|
||||||
}
|
|
||||||
span:before {
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
top: -8px;
|
|
||||||
left: -36px;
|
|
||||||
right: -8px;
|
|
||||||
bottom: -4px;
|
|
||||||
z-index: -1;
|
|
||||||
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n2-treenode {
|
|
||||||
& > .el-name {
|
|
||||||
white-space: nowrap;
|
|
||||||
width: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.drag-source {
|
|
||||||
& > .el-name {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .el-name:after {
|
|
||||||
position: absolute;
|
|
||||||
content: url('/images/${_VERSION}/icon_drag_source.svg');
|
|
||||||
filter: var(--colorize);
|
|
||||||
top: -1px;
|
|
||||||
right: -24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.drag-target {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
& > .el-name {
|
|
||||||
anchor-name: --name;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .el-name:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
border: 2px dashed #888;
|
|
||||||
|
|
||||||
top: calc(anchor(--name top) - 12px);
|
|
||||||
right: calc(anchor(--name right) - 8px);
|
|
||||||
bottom: calc(anchor(--name bottom) - 8px);
|
|
||||||
left: calc(anchor(--name left) - 40px);
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .el-drag-icon {
|
|
||||||
display: block;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
z-index: 16384;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div data-el="expand-toggle" class="expand-toggle">
|
|
||||||
<img data-el="expand" draggable="false">
|
|
||||||
</div>
|
|
||||||
<div data-el="name" class="name"><span></span></div>
|
|
||||||
<div data-el="children" class="children"></div>
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
constructor(sidebar, node, parent) {//{{{
|
|
||||||
super()
|
|
||||||
this.setAttribute('draggable', 'true')
|
|
||||||
this.classList.add('node')
|
|
||||||
|
|
||||||
this.sidebar = sidebar
|
|
||||||
this.node = node
|
|
||||||
this.parent = parent
|
|
||||||
|
|
||||||
this.children_populated = false
|
|
||||||
this.rendered = false
|
|
||||||
this.dragNode = null
|
|
||||||
|
|
||||||
this.elExpandToggle.addEventListener('click', event => {
|
|
||||||
if (this.node.hasChildren())
|
|
||||||
this.expandNode(event)
|
|
||||||
else
|
|
||||||
_mbus.dispatch('TREE_NODE_SELECTED', this.node)
|
|
||||||
})
|
|
||||||
this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
|
|
||||||
|
|
||||||
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => {
|
|
||||||
this.render(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Drag-and-dropping of nodes
|
|
||||||
this.addEventListener('dragstart', event => this.dragStart(event))
|
|
||||||
this.addEventListener('dragend', event => this.dragEnd(event))
|
|
||||||
this.addEventListener('dragover', event => this.dragOver(event))
|
|
||||||
this.addEventListener('drop', event => this.dragDrop(event))
|
|
||||||
this.elName.addEventListener('dragenter', event => this.dragEnter(event))
|
|
||||||
this.elName.addEventListener('dragleave', event => this.dragLeave(event))
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
dragStart(e) {// {{{
|
|
||||||
if (this.node.isModified()) {
|
|
||||||
alert('Save note before moving it.')
|
|
||||||
e.stopPropagation()
|
|
||||||
e.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.classList.add('drag-source')
|
|
||||||
const blankPixel = new Image()
|
|
||||||
blankPixel.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
|
||||||
e.dataTransfer.setDragImage(blankPixel, 0, 0)
|
|
||||||
e.dataTransfer.allowedEffects = 'none'
|
|
||||||
e.stopPropagation()
|
|
||||||
_app.dragIcon.setSource(this)
|
|
||||||
_app.dragIcon.start()
|
|
||||||
}// }}}
|
|
||||||
dragEnd(e) {// {{{
|
|
||||||
this.classList.remove('drag-source')
|
|
||||||
_app.dragIcon.end()
|
|
||||||
e.stopPropagation()
|
|
||||||
}// }}}
|
|
||||||
dragOver(e) {// {{{
|
|
||||||
e.dataTransfer.dropEffect = 'move'
|
|
||||||
e.preventDefault()
|
|
||||||
}// }}}
|
|
||||||
async dragDrop(e) {// {{{
|
|
||||||
try {
|
|
||||||
e.stopPropagation()
|
|
||||||
const sourceNode = _app.dragIcon.getSource()
|
|
||||||
|
|
||||||
// Abort if user drops the node back on itself.
|
|
||||||
if (sourceNode.node.UUID === this.node.UUID)
|
|
||||||
return
|
|
||||||
|
|
||||||
await _app.moveNode(sourceNode.node, this.node.UUID)
|
|
||||||
|
|
||||||
_app.sidebar.setNodeExpanded(this, true)
|
|
||||||
await this.render(true, true)
|
|
||||||
await sourceNode.render(true, true)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
alert(e)
|
|
||||||
} finally {
|
|
||||||
this.dragLeave(e)
|
|
||||||
}
|
|
||||||
}// }}}
|
|
||||||
dragEnter(e) {// {{{
|
|
||||||
const targetNode = e.target.closest('n2-treenode')
|
|
||||||
if (targetNode.classList.contains('drag-source'))
|
|
||||||
return
|
|
||||||
e.stopPropagation()
|
|
||||||
_app.dragIcon.icon('ok')
|
|
||||||
this.classList.add('drag-target')
|
|
||||||
}// }}}
|
|
||||||
dragLeave(e) {// {{{
|
|
||||||
e.stopPropagation()
|
|
||||||
e.dataTransfer.dropEffect = 'none'
|
|
||||||
e.dataTransfer.setDragImage(N2TreeNode.DRAG_ICON, -16, 8)
|
|
||||||
_app.dragIcon.icon('')
|
|
||||||
this.classList.remove('drag-target')
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
async expandNode(event) {// {{{
|
|
||||||
const expanded = _app.sidebar.getNodeExpanded(this.node.UUID)
|
|
||||||
|
|
||||||
if (event.shiftKey) {
|
|
||||||
_app.sidebar.recursiveExpand(this.node, !expanded)
|
|
||||||
} else {
|
|
||||||
_app.sidebar.setNodeExpanded(this.node, !expanded)
|
|
||||||
}
|
|
||||||
}// }}}
|
|
||||||
async fetchChildren(force_fetch) {//{{{
|
|
||||||
if (this.children_populated && !force_fetch)
|
|
||||||
return
|
|
||||||
|
|
||||||
await this.node.fetchChildren()
|
|
||||||
this.children_populated = true
|
|
||||||
}//}}}
|
|
||||||
async render(force_update, force_refetch_children) {//{{{
|
|
||||||
if (this.rendered && force_update !== true)
|
|
||||||
return this
|
|
||||||
|
|
||||||
if (this.sidebar.getNodeExpanded(this.node.UUID) || force_refetch_children)
|
|
||||||
await this.fetchChildren(force_refetch_children)
|
|
||||||
|
|
||||||
// Update the name and selected status.
|
|
||||||
this.elName.querySelector('span').innerText = this.node.get('Name')
|
|
||||||
|
|
||||||
if (this.sidebar.isSelected(this.node))
|
|
||||||
this.elName.classList.add('selected')
|
|
||||||
else
|
|
||||||
this.elName.classList.remove('selected')
|
|
||||||
|
|
||||||
// Update expansion state
|
|
||||||
const expanded = this.node.hasChildren() && this.sidebar.getNodeExpanded(this.node.UUID)
|
|
||||||
if (expanded) {
|
|
||||||
this.elChildren.classList.add('expanded')
|
|
||||||
this.elChildren.classList.remove('collapsed')
|
|
||||||
} else {
|
|
||||||
this.elChildren.classList.remove('expanded')
|
|
||||||
this.elChildren.classList.add('collapsed')
|
|
||||||
}
|
|
||||||
|
|
||||||
// The expand icon <img> is only changed to not get a flickering when re-rendering.
|
|
||||||
if (this.node.UUID === ROOT_NODE)
|
|
||||||
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/icon_home.svg`)
|
|
||||||
|
|
||||||
else if (this.node.UUID === DELETED_NODE) {
|
|
||||||
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf_deleted.svg`)
|
|
||||||
this.elExpand.classList.add('deleted')
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (this.node.UUID === ORPHANED_NODE) {
|
|
||||||
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf_orphaned.svg`)
|
|
||||||
this.elExpand.classList.add('deleted')
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (!this.node.hasChildren())
|
|
||||||
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf.svg`)
|
|
||||||
else if (this.sidebar.getNodeExpanded(this.node.UUID))
|
|
||||||
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/expanded.svg`)
|
|
||||||
else
|
|
||||||
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/collapsed.svg`)
|
|
||||||
|
|
||||||
// Should children be rendered?
|
|
||||||
let children = []
|
|
||||||
if (expanded)
|
|
||||||
children = this.node.Children.map(node => {
|
|
||||||
let treenode = this.sidebar.treeNodeComponents[node.UUID]
|
|
||||||
if (treenode === undefined) {
|
|
||||||
treenode = new N2TreeNode(this.sidebar, node, this)
|
|
||||||
this.sidebar.treeNodeComponents[node.UUID] = treenode
|
|
||||||
}
|
|
||||||
return treenode
|
|
||||||
})
|
|
||||||
|
|
||||||
const renderedChildren = []
|
|
||||||
for (const c of children)
|
|
||||||
renderedChildren.push(await c.render())
|
|
||||||
this.elChildren.replaceChildren(...renderedChildren)
|
|
||||||
|
|
||||||
this.rendered = true
|
|
||||||
return this
|
|
||||||
}//}}}
|
|
||||||
|
|
||||||
setImgSrc(img, newSrc) {// {{{
|
|
||||||
if (img.getAttribute('src') === newSrc)
|
|
||||||
return
|
|
||||||
img.setAttribute('src', newSrc)
|
|
||||||
}// }}}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpecialNodeDeleted extends N2TreeNode {
|
|
||||||
constructor(sidebar, node, parent) {//{{{
|
|
||||||
super(sidebar, node, parent)
|
|
||||||
this.removeAttribute('draggable')
|
|
||||||
}//}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpecialNodeOrphaned extends N2TreeNode {
|
|
||||||
constructor(sidebar, node, parent) {//{{{
|
|
||||||
super(sidebar, node, parent)
|
|
||||||
this.removeAttribute('draggable')
|
|
||||||
}//}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('n2-sidebar', N2Sidebar)
|
|
||||||
customElements.define('n2-treenode', N2TreeNode)
|
|
||||||
customElements.define('n2-specialnodedeleted', SpecialNodeDeleted)
|
|
||||||
customElements.define('n2-specialnodeorphaned', SpecialNodeOrphaned)
|
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
|
||||||
|
|
@ -9,6 +9,9 @@ export class Sync {
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
async run() {//{{{
|
async run() {//{{{
|
||||||
|
// XXX - Delete me
|
||||||
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let duration = 0 // in ms
|
let duration = 0 // in ms
|
||||||
|
|
||||||
|
|
@ -17,12 +20,10 @@ export class Sync {
|
||||||
const state = await nodeStore.getAppState('latest_sync_node')
|
const state = await nodeStore.getAppState('latest_sync_node')
|
||||||
const oldMax = (state?.value ? state.value : 0)
|
const oldMax = (state?.value ? state.value : 0)
|
||||||
|
|
||||||
let nodeCountDownload = await this.getNodeCount(oldMax)
|
let nodeCount = await this.getNodeCount(oldMax)
|
||||||
let nodeCountUpload = await nodeStore.sendQueue.count()
|
nodeCount += await nodeStore.sendQueue.count()
|
||||||
|
|
||||||
_mbus.dispatch('SYNC_START')
|
_mbus.dispatch('SYNC_COUNT', { count: nodeCount })
|
||||||
_mbus.dispatch('SYNC_DOWNLOAD_COUNT', { count: nodeCountDownload })
|
|
||||||
_mbus.dispatch('SYNC_UPLOAD_COUNT', { count: nodeCountUpload })
|
|
||||||
|
|
||||||
await this.nodesFromServer(oldMax)
|
await this.nodesFromServer(oldMax)
|
||||||
.then(durationNodes => {
|
.then(durationNodes => {
|
||||||
|
|
@ -30,7 +31,6 @@ export class Sync {
|
||||||
console.log(`Total time: ${Math.round(1000 * durationNodes) / 1000}s`)
|
console.log(`Total time: ${Math.round(1000 * durationNodes) / 1000}s`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Uploads of modified nodes to server.
|
|
||||||
await this.nodesToServer()
|
await this.nodesToServer()
|
||||||
} finally {
|
} finally {
|
||||||
_mbus.dispatch('SYNC_DONE')
|
_mbus.dispatch('SYNC_DONE')
|
||||||
|
|
@ -81,16 +81,15 @@ export class Sync {
|
||||||
|
|
||||||
handled++
|
handled++
|
||||||
if (handled % 100 === 0)
|
if (handled % 100 === 0)
|
||||||
_mbus.dispatch('SYNC_DOWNLOADED', { handled })
|
_mbus.dispatch('SYNC_HANDLED', { handled })
|
||||||
}
|
}
|
||||||
|
|
||||||
} while (res.Continue)
|
} while (res.Continue)
|
||||||
_mbus.dispatch('SYNC_DOWNLOADED', { handled })
|
_mbus.dispatch('SYNC_HANDLED', { handled })
|
||||||
|
|
||||||
nodeStore.setAppState('latest_sync_node', currMax)
|
nodeStore.setAppState('latest_sync_node', currMax)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('sync node tree', e)
|
console.log('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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,80 +169,59 @@ export class N2SyncProgress extends CustomHTMLElement {
|
||||||
static {// {{{
|
static {// {{{
|
||||||
this.tmpl = document.createElement('template')
|
this.tmpl = document.createElement('template')
|
||||||
this.tmpl.innerHTML = `
|
this.tmpl.innerHTML = `
|
||||||
<img src="/images/${_VERSION}/icon_transfer.svg">
|
<progress data-el="progress" min=0 max=137 value=0></progress>
|
||||||
<div data-el="download-transferred" class="count">0</div> <div>/</div> <div data-el="download-total">0</div>
|
<div data-el="count" class="count">0 / 0</div>
|
||||||
<div data-el="upload-transferred" class="count">0</div> <div>/</div> <div data-el="upload-total">0</div>
|
|
||||||
`
|
`
|
||||||
}// }}}
|
}// }}}
|
||||||
constructor() {//{{{
|
constructor() {//{{{
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.reset()
|
this.reset()
|
||||||
_mbus.subscribe('SYNC_START', () => this.reset())
|
_mbus.subscribe('SYNC_COUNT', event => this.progressHandler(event))
|
||||||
_mbus.subscribe('SYNC_DOWNLOAD_COUNT', event => this.progressHandler(event))
|
_mbus.subscribe('SYNC_HANDLED', event => this.progressHandler(event))
|
||||||
_mbus.subscribe('SYNC_UPLOAD_COUNT', event => this.progressHandler(event))
|
|
||||||
_mbus.subscribe('SYNC_DOWNLOADED', event => this.progressHandler(event))
|
|
||||||
_mbus.subscribe('SYNC_UPLOADED', event => this.progressHandler(event))
|
|
||||||
_mbus.subscribe('SYNC_DONE', event => this.progressHandler(event))
|
_mbus.subscribe('SYNC_DONE', event => this.progressHandler(event))
|
||||||
}//}}}
|
}//}}}
|
||||||
reset() {//{{{
|
reset() {//{{{
|
||||||
this.classList.remove('ok')
|
|
||||||
this.state = {
|
this.state = {
|
||||||
nodesToDownload: 0,
|
nodesToSync: 0,
|
||||||
nodesToUpload: 0,
|
nodesSynced: 0,
|
||||||
nodesDowloaded: 0,
|
|
||||||
nodesUploaded: 0,
|
|
||||||
}
|
}
|
||||||
this.render()
|
|
||||||
}//}}}
|
}//}}}
|
||||||
progressHandler(event) {//{{{
|
progressHandler(event) {//{{{
|
||||||
const eventData = event.detail.data
|
const eventData = event.detail.data
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'SYNC_DOWNLOAD_COUNT':
|
case 'SYNC_COUNT':
|
||||||
this.state.nodesToDownload = eventData.count
|
this.state.nodesToSync = eventData.count
|
||||||
this.setSyncState(true)
|
this.setSyncState(true)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'SYNC_UPLOAD_COUNT':
|
case 'SYNC_HANDLED':
|
||||||
this.state.nodesToUpload = eventData.count
|
this.state.nodesSynced = eventData.handled
|
||||||
this.setSyncState(true)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'SYNC_DOWNLOADED':
|
|
||||||
this.state.nodesDowloaded = eventData.handled
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'SYNC_UPLOADED':
|
|
||||||
this.state.nodesUploaded += eventData.count
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'SYNC_DONE':
|
case 'SYNC_DONE':
|
||||||
this.classList.add('ok')
|
|
||||||
|
|
||||||
// Hides the progress bar.
|
// Hides the progress bar.
|
||||||
this.setSyncState(false)
|
this.setSyncState(false)
|
||||||
|
|
||||||
// Don't update anything if nothing was synced.
|
// Don't update anything if nothing was synced.
|
||||||
if (this.state.nodesDowloaded === 0)
|
if (this.state.nodesSynced === 0)
|
||||||
break
|
break
|
||||||
|
|
||||||
// Reload the tree nodes to reflect the new/updated nodes.
|
// Reload the tree nodes to reflect the new/updated nodes.
|
||||||
window._app.sidebar.reset()
|
window._app.tree.reset()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
this.render()
|
this.render()
|
||||||
}//}}}
|
}//}}}
|
||||||
render() {//{{{
|
render() {//{{{
|
||||||
this.elDownloadTransferred.innerText = this.state.nodesDowloaded
|
this.elProgress.max = this.state.nodesToSync
|
||||||
this.elDownloadTotal.innerText = this.state.nodesToDownload
|
this.elProgress.value = this.state.nodesSynced
|
||||||
|
this.elCount.innerText = `${this.state.nodesSynced} / ${this.state.nodesToSync}`
|
||||||
this.elUploadTransferred.innerText = this.state.nodesUploaded
|
|
||||||
this.elUploadTotal.innerText = this.state.nodesToUpload
|
|
||||||
}//}}}
|
}//}}}
|
||||||
setSyncState(state) {// {{{
|
setSyncState(state) {// {{{
|
||||||
if (state)
|
if (state)
|
||||||
this.classList.add('show')
|
this.classList.add('show')
|
||||||
else
|
else
|
||||||
// Give the user a chance to see what it ended on.
|
|
||||||
setTimeout(() => this.classList.remove('show'), 1500)
|
setTimeout(() => this.classList.remove('show'), 1500)
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
473
static/js/tree.mjs
Normal file
|
|
@ -0,0 +1,473 @@
|
||||||
|
import { ROOT_NODE } from 'node_store'
|
||||||
|
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||||
|
import { Color, Solver } from './lib/css_colorize.mjs'
|
||||||
|
|
||||||
|
export class N2Tree extends CustomHTMLElement {
|
||||||
|
static {// {{{
|
||||||
|
this.tmpl = document.createElement('template')
|
||||||
|
this.tmpl.innerHTML = `
|
||||||
|
<div data-el="logo" id="logo">
|
||||||
|
<img src="/images/${_VERSION}/logo_small.svg" />
|
||||||
|
<img src="/images/${_VERSION}/logo.svg" />
|
||||||
|
<img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
|
||||||
|
</div>
|
||||||
|
<div class="icons">
|
||||||
|
<img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" />
|
||||||
|
<img data-el="settings" class='settings' src="/images/${_VERSION}/icon_settings.svg" />
|
||||||
|
</div>
|
||||||
|
<div data-el="treenodes"></div>
|
||||||
|
`
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
constructor() {// {{{
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.id = 'tree-nodes'
|
||||||
|
this.tabIndex = 0
|
||||||
|
|
||||||
|
this.treeNodeComponents = {}
|
||||||
|
this.treeTrunk = []
|
||||||
|
this.expandedNodes = {} // keyed on UUID
|
||||||
|
this.selectedNode = null
|
||||||
|
this.rendered = false
|
||||||
|
|
||||||
|
this.addEventListener('keydown', event => this.keyHandler(event))
|
||||||
|
this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search'))
|
||||||
|
this.elSync.addEventListener('click', () => _sync.run())
|
||||||
|
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
|
||||||
|
|
||||||
|
_mbus.subscribe('NODE_MODIFIED', ({ detail }) => {
|
||||||
|
const node = detail.data.node
|
||||||
|
const treenode = this.treeNodeComponents[node.get('UUID')]
|
||||||
|
|
||||||
|
if (!treenode)
|
||||||
|
return
|
||||||
|
|
||||||
|
treenode.node = node
|
||||||
|
treenode.render(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.populateFirstLevel()
|
||||||
|
|
||||||
|
/* XXX - set color */
|
||||||
|
let color = new Color(255, 96, 80)
|
||||||
|
let solver = new Solver(color)
|
||||||
|
let result = solver.solve()
|
||||||
|
this.elSettings.style.filter = result.filter
|
||||||
|
}// }}}
|
||||||
|
render() {// {{{
|
||||||
|
if (this.rendered)
|
||||||
|
alert('Tree should only be rendered once.')
|
||||||
|
|
||||||
|
for (const node of this.treeTrunk) {
|
||||||
|
const treenode = new N2TreeNode(this, node)
|
||||||
|
this.treeNodeComponents[node.UUID] = treenode
|
||||||
|
this.elTreenodes.appendChild(treenode.render())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rendered = true
|
||||||
|
return this
|
||||||
|
}// }}}
|
||||||
|
reset() {// {{{
|
||||||
|
console.log('tree reset')
|
||||||
|
this.treeNodeComponents = {}
|
||||||
|
this.treeTrunk = []
|
||||||
|
this.rendered = false
|
||||||
|
this.elTreenodes.replaceChildren()
|
||||||
|
this.populateFirstLevel()
|
||||||
|
}// }}}
|
||||||
|
populateFirstLevel() {//{{{
|
||||||
|
nodeStore.get(ROOT_NODE)
|
||||||
|
.then(node => node.fetchChildren())
|
||||||
|
.then(children => {
|
||||||
|
this.treeNodeComponents = {}
|
||||||
|
this.treeTrunk = []
|
||||||
|
for (const node of children) {
|
||||||
|
// The root node isn't supposed to be shown in the tree.
|
||||||
|
if (node.UUID === ROOT_NODE)
|
||||||
|
continue
|
||||||
|
if (node.ParentUUID === ROOT_NODE)
|
||||||
|
this.treeTrunk.push(node)
|
||||||
|
}
|
||||||
|
_mbus.dispatch('TREE_TRUNK_FETCHED')
|
||||||
|
})
|
||||||
|
.catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) })
|
||||||
|
}//}}}
|
||||||
|
getNodeExpanded(UUID) {//{{{
|
||||||
|
if (this.expandedNodes[UUID] === undefined)
|
||||||
|
this.expandedNodes[UUID] = false
|
||||||
|
return this.expandedNodes[UUID]
|
||||||
|
}//}}}
|
||||||
|
setNodeExpanded(node, value) {//{{{
|
||||||
|
let expanded = this.expandedNodes[node.UUID]
|
||||||
|
|
||||||
|
if (expanded === undefined) {
|
||||||
|
this.expandedNodes[node.UUID] = false
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expanded === value)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.expandedNodes[node.UUID] = value
|
||||||
|
_mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value)
|
||||||
|
}//}}}
|
||||||
|
setSelected(node, dontExpand) {//{{{
|
||||||
|
if (node === undefined)
|
||||||
|
return
|
||||||
|
|
||||||
|
// The previously selected node, if any, needs to be rerendered
|
||||||
|
// to not retain its 'selected' class.
|
||||||
|
const prevUUID = this.selectedNode?.UUID
|
||||||
|
this.selectedNode = node
|
||||||
|
if (prevUUID)
|
||||||
|
this.treeNodeComponents[prevUUID]?.render(true)
|
||||||
|
|
||||||
|
// And now the newly selected node is rerendered.
|
||||||
|
this.treeNodeComponents[node.UUID]?.render(true)
|
||||||
|
|
||||||
|
if (!dontExpand)
|
||||||
|
this.setNodeExpanded(node, true)
|
||||||
|
}//}}}
|
||||||
|
isSelected(node) {//{{{
|
||||||
|
return this.selectedNode?.UUID === node.UUID
|
||||||
|
}//}}}
|
||||||
|
|
||||||
|
async keyHandler(event) {//{{{
|
||||||
|
let handled = true
|
||||||
|
const n = this.selectedNode
|
||||||
|
const Space = ' '
|
||||||
|
|
||||||
|
// This handler would otherwise react to stuff like Ctrl+L.
|
||||||
|
if (event.ctrlKey || event.altKey)
|
||||||
|
return
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
// Space and enter is toggling expansion.
|
||||||
|
// Holding shift down does it recursively.
|
||||||
|
case Space:
|
||||||
|
case 'Enter':
|
||||||
|
const expanded = this.getNodeExpanded(n.UUID)
|
||||||
|
if (event.shiftKey) {
|
||||||
|
this.recursiveExpand(n, !expanded)
|
||||||
|
} else {
|
||||||
|
this.setNodeExpanded(n, !expanded)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
async navigateLeft(n) {//{{{
|
||||||
|
if (n === null || n === undefined)
|
||||||
|
return
|
||||||
|
|
||||||
|
const expanded = this.getNodeExpanded(n.UUID)
|
||||||
|
if (expanded && n.hasChildren()) {
|
||||||
|
this.setNodeExpanded(n, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) {
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const siblingBefore = n.getSiblingBefore()
|
||||||
|
const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
|
||||||
|
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
||||||
|
const siblingAbove = this.getLastExpandedNode(siblingBefore)
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
}//}}}
|
||||||
|
async navigateRight(n) {//{{{
|
||||||
|
if (n === null || n === undefined)
|
||||||
|
return
|
||||||
|
|
||||||
|
const siblingAfter = n.getSiblingAfter()
|
||||||
|
const expanded = this.getNodeExpanded(n.UUID)
|
||||||
|
|
||||||
|
if (!expanded && n.hasChildren()) {
|
||||||
|
this.setNodeExpanded(n, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expanded && n.hasChildren()) {
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.isLastSibling()) {
|
||||||
|
const nextNode = this.getParentWithNextSibling(n)
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
}//}}}
|
||||||
|
async navigateUp(n) {//{{{
|
||||||
|
if (n === null || n === undefined)
|
||||||
|
return
|
||||||
|
|
||||||
|
let parent = null
|
||||||
|
const siblingBefore = n.getSiblingBefore()
|
||||||
|
let siblingExpanded = false
|
||||||
|
if (siblingBefore !== null)
|
||||||
|
siblingExpanded = this.getNodeExpanded(siblingBefore.UUID)
|
||||||
|
|
||||||
|
if (n.isFirstSibling()) {
|
||||||
|
parent = n.getParent()
|
||||||
|
if (parent?.UUID === ROOT_NODE)
|
||||||
|
return
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siblingBefore) {
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
async navigateDown(n) {//{{{
|
||||||
|
if (n === null || n === undefined)
|
||||||
|
return
|
||||||
|
|
||||||
|
const nodeExpanded = this.getNodeExpanded(n.UUID)
|
||||||
|
|
||||||
|
// Last node, not expanded, so it matters not whether it has children or not.
|
||||||
|
// Traverse upward to nearest parent with next sibling.
|
||||||
|
if (!nodeExpanded && n.isLastSibling()) {
|
||||||
|
const wantedNode = this.getParentWithNextSibling(n)
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
|
||||||
|
const wantedNode = this.getParentWithNextSibling(n)
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node not expanded. Go to this node's next sibling.
|
||||||
|
// GoToNode will abort if given null.
|
||||||
|
if (!nodeExpanded || !n.hasChildren()) {
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node is expanded.
|
||||||
|
// Children will be visually beneath this node, if any.
|
||||||
|
if (nodeExpanded && n.hasChildren()) {
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: false, dontExpand: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
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 })
|
||||||
|
}//}}}
|
||||||
|
async navigateBottom() {//{{{
|
||||||
|
const root = await nodeStore.get(ROOT_NODE)
|
||||||
|
if (root.Children.length === 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
const toplevel = root.Children[root.Children.length - 1]
|
||||||
|
const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID)
|
||||||
|
|
||||||
|
if (toplevelExpanded) {
|
||||||
|
const lastnode = this.getLastExpandedNode(toplevel)
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
} else
|
||||||
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
|
||||||
|
}//}}}
|
||||||
|
|
||||||
|
getParentWithNextSibling(node) {//{{{
|
||||||
|
let currNode = node
|
||||||
|
while (currNode !== null && currNode.UUID !== ROOT_NODE && currNode.getSiblingAfter() === null) {
|
||||||
|
currNode = currNode.getParent()
|
||||||
|
}
|
||||||
|
return currNode?.getSiblingAfter()
|
||||||
|
}//}}}
|
||||||
|
getLastExpandedNode(node) {//{{{
|
||||||
|
let currNode = node
|
||||||
|
while (this.getNodeExpanded(currNode.UUID) && currNode.hasChildren()) {
|
||||||
|
currNode = currNode.Children[currNode.Children.length - 1]
|
||||||
|
}
|
||||||
|
return currNode
|
||||||
|
}//}}}
|
||||||
|
async recursiveExpand(node, state) {//{{{
|
||||||
|
if (state)
|
||||||
|
await this.setNodeExpanded(node, true)
|
||||||
|
|
||||||
|
for (const child of node.Children)
|
||||||
|
await this.recursiveExpand(child, state)
|
||||||
|
|
||||||
|
if (!state)
|
||||||
|
await this.setNodeExpanded(node, false)
|
||||||
|
}//}}}
|
||||||
|
async makeVisible(node) {// {{{
|
||||||
|
const treenode = this.treeNodeComponents[node.UUID]
|
||||||
|
|
||||||
|
const ancestors = await nodeStore.getNodeAncestry(node)
|
||||||
|
for (const ancestor of ancestors.reverse()) {
|
||||||
|
this.setNodeExpanded(ancestor, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ROOT_NODE for example hasn't got a treenode.
|
||||||
|
treenode?.scrollIntoView({ block: 'nearest' })
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
customElements.define('n2-tree', N2Tree)
|
||||||
|
|
||||||
|
export class N2TreeNode extends CustomHTMLElement {
|
||||||
|
static {// {{{
|
||||||
|
this.tmpl = document.createElement('template')
|
||||||
|
this.tmpl.innerHTML = `
|
||||||
|
<div data-el="expand-toggle" class="expand-toggle">
|
||||||
|
<img data-el="expand">
|
||||||
|
</div>
|
||||||
|
<div data-el="name" class="name"></div>
|
||||||
|
<div data-el="children" class="children"></div>
|
||||||
|
`
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
constructor(tree, node, parent) {//{{{
|
||||||
|
super()
|
||||||
|
this.classList.add('node')
|
||||||
|
|
||||||
|
this.tree = tree
|
||||||
|
this.node = node
|
||||||
|
this.parent = parent
|
||||||
|
|
||||||
|
this.children_populated = false
|
||||||
|
this.rendered = false
|
||||||
|
|
||||||
|
this.elExpandToggle.addEventListener('click', () => this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID)))
|
||||||
|
this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
|
||||||
|
|
||||||
|
_mbus.subscribe(`NODE_CHILDREN_FETCHED_${node.UUID}`, () => {
|
||||||
|
this.render(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state => {
|
||||||
|
this.render(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.node.Level === 0 || this.tree.getNodeExpanded(this.node.UUID))
|
||||||
|
this.fetchChildren()
|
||||||
|
}// }}}
|
||||||
|
async fetchChildren() {//{{{
|
||||||
|
await this.node.fetchChildren()
|
||||||
|
this.children_populated = true
|
||||||
|
}//}}}
|
||||||
|
render(force_update) {//{{{
|
||||||
|
if (this.rendered && force_update !== true)
|
||||||
|
return this
|
||||||
|
|
||||||
|
// Fetch the next level of children if the parent tree node is expanded and our children thus will be visible.
|
||||||
|
const expanded = this.node.Children.length > 0 && this.tree.getNodeExpanded(this.node.UUID)
|
||||||
|
|
||||||
|
if (!this.children_populated && this.tree.getNodeExpanded(this.parent?.node.UUID)) {
|
||||||
|
this.node.fetchChildren().then(() => this.children_populated = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the name and selected status
|
||||||
|
this.elName.innerText = this.node.get('Name')
|
||||||
|
if (this.tree.isSelected(this.node))
|
||||||
|
this.elName.classList.add('selected')
|
||||||
|
else
|
||||||
|
this.elName.classList.remove('selected')
|
||||||
|
|
||||||
|
// Update expansion state
|
||||||
|
if (expanded) {
|
||||||
|
this.elChildren.classList.add('expanded')
|
||||||
|
this.elChildren.classList.remove('collapsed')
|
||||||
|
} else {
|
||||||
|
this.elChildren.classList.remove('expanded')
|
||||||
|
this.elChildren.classList.add('collapsed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// The expand icon <img> is only changed to not get a flickering when re-rendering.
|
||||||
|
if (this.node.Children.length === 0)
|
||||||
|
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf.svg`)
|
||||||
|
else if (this.tree.getNodeExpanded(this.node.UUID))
|
||||||
|
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/expanded.svg`)
|
||||||
|
else
|
||||||
|
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/collapsed.svg`)
|
||||||
|
|
||||||
|
// Should children be rendered?
|
||||||
|
this.elChildren.innerHTML = ''
|
||||||
|
let children = []
|
||||||
|
if (expanded)
|
||||||
|
children = this.node.Children.map(node => {
|
||||||
|
let treenode = this.tree.treeNodeComponents[node.UUID]
|
||||||
|
if (treenode === undefined) {
|
||||||
|
treenode = new N2TreeNode(this.tree, node, this)
|
||||||
|
this.tree.treeNodeComponents[node.UUID] = treenode
|
||||||
|
}
|
||||||
|
return treenode
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const c of children)
|
||||||
|
this.elChildren.appendChild(c.render())
|
||||||
|
|
||||||
|
this.rendered = true
|
||||||
|
return this
|
||||||
|
}//}}}
|
||||||
|
|
||||||
|
setImgSrc(img, newSrc) {// {{{
|
||||||
|
if (img.getAttribute('src') === newSrc)
|
||||||
|
return
|
||||||
|
img.setAttribute('src', newSrc)
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
customElements.define('n2-treenode', N2TreeNode)
|
||||||
|
|
||||||
|
// vim: foldmethod=marker
|
||||||
|
|
@ -6,45 +6,35 @@ const CACHED_ASSETS = [
|
||||||
'/css/{{ .VERSION }}/main.css',
|
'/css/{{ .VERSION }}/main.css',
|
||||||
'/css/{{ .VERSION }}/markdown.css',
|
'/css/{{ .VERSION }}/markdown.css',
|
||||||
'/css/{{ .VERSION }}/notes2.css',
|
'/css/{{ .VERSION }}/notes2.css',
|
||||||
'/css/{{ .VERSION }}/page_history.css',
|
|
||||||
'/css/{{ .VERSION }}/theme.css',
|
'/css/{{ .VERSION }}/theme.css',
|
||||||
|
|
||||||
'/images/{{ .VERSION }}/collapsed.svg',
|
'/images/{{ .VERSION }}/collapsed.svg',
|
||||||
'/images/{{ .VERSION }}/expanded.svg',
|
'/images/{{ .VERSION }}/expanded.svg',
|
||||||
'/images/{{ .VERSION }}/icon_history.svg',
|
|
||||||
'/images/{{ .VERSION }}/icon_home.svg',
|
|
||||||
'/images/{{ .VERSION }}/icon_markdown_hollow.svg',
|
'/images/{{ .VERSION }}/icon_markdown_hollow.svg',
|
||||||
'/images/{{ .VERSION }}/icon_markdown.svg',
|
'/images/{{ .VERSION }}/icon_markdown.svg',
|
||||||
'/images/{{ .VERSION }}/icon_refresh.svg',
|
'/images/{{ .VERSION }}/icon_refresh.svg',
|
||||||
'/images/{{ .VERSION }}/icon_save_disabled.svg',
|
'/images/{{ .VERSION }}/icon_save_disabled.svg',
|
||||||
'/images/{{ .VERSION }}/icon_save.svg',
|
|
||||||
'/images/{{ .VERSION }}/icon_search.svg',
|
'/images/{{ .VERSION }}/icon_search.svg',
|
||||||
'/images/{{ .VERSION }}/icon_settings.svg',
|
|
||||||
'/images/{{ .VERSION }}/icon_table.svg',
|
|
||||||
'/images/{{ .VERSION }}/leaf.svg',
|
'/images/{{ .VERSION }}/leaf.svg',
|
||||||
'/images/{{ .VERSION }}/logo_small.svg',
|
|
||||||
'/images/{{ .VERSION }}/logo.svg',
|
'/images/{{ .VERSION }}/logo.svg',
|
||||||
|
|
||||||
'/js/{{ .VERSION }}/api.mjs',
|
'/js/{{ .VERSION }}/api.mjs',
|
||||||
'/js/{{ .VERSION }}/app.mjs',
|
'/js/{{ .VERSION }}/app.mjs',
|
||||||
'/js/{{ .VERSION }}/checklist.mjs',
|
'/js/{{ .VERSION }}/checklist.mjs',
|
||||||
'/js/{{ .VERSION }}/crypto.mjs',
|
'/js/{{ .VERSION }}/crypto.mjs',
|
||||||
'/js/{{ .VERSION }}/file.mjs',
|
|
||||||
'/js/{{ .VERSION }}/key.mjs',
|
'/js/{{ .VERSION }}/key.mjs',
|
||||||
'/js/{{ .VERSION }}/lib/css_colorize.mjs',
|
|
||||||
'/js/{{ .VERSION }}/lib/custom_html_element.mjs',
|
'/js/{{ .VERSION }}/lib/custom_html_element.mjs',
|
||||||
'/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js',
|
'/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js',
|
||||||
'/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js',
|
'/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js',
|
||||||
'/js/{{ .VERSION }}/lib/sjcl.js',
|
'/js/{{ .VERSION }}/lib/sjcl.js',
|
||||||
'/js/{{ .VERSION }}/marked_position.mjs',
|
'/js/{{ .VERSION }}/marked_position.mjs',
|
||||||
'/js/{{ .VERSION }}/mbus.mjs',
|
'/js/{{ .VERSION }}/mbus.mjs',
|
||||||
'/js/{{ .VERSION }}/node_store.mjs',
|
|
||||||
'/js/{{ .VERSION }}/notes2.mjs',
|
|
||||||
'/js/{{ .VERSION }}/page_history.mjs',
|
|
||||||
'/js/{{ .VERSION }}/page_node.mjs',
|
'/js/{{ .VERSION }}/page_node.mjs',
|
||||||
'/js/{{ .VERSION }}/page_storage.mjs',
|
'/js/{{ .VERSION }}/page_storage.mjs',
|
||||||
'/js/{{ .VERSION }}/sidebar.mjs',
|
'/js/{{ .VERSION }}/node_store.mjs',
|
||||||
|
'/js/{{ .VERSION }}/notes2.mjs',
|
||||||
'/js/{{ .VERSION }}/sync.mjs',
|
'/js/{{ .VERSION }}/sync.mjs',
|
||||||
|
'/js/{{ .VERSION }}/tree.mjs',
|
||||||
]
|
]
|
||||||
|
|
||||||
async function precache() {
|
async function precache() {
|
||||||
|
|
@ -120,13 +110,9 @@ self.addEventListener('activate', event => {
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener('fetch', event => {
|
self.addEventListener('fetch', event => {
|
||||||
// The fetch event is also seeing requests to other domains.
|
// console.debug('SERVICE WORKER: fetch', event.request.url)
|
||||||
// Just let the browser handle those for itself.
|
|
||||||
const ourDomain = event.request.url.startsWith(self.location.origin)
|
|
||||||
if (!ourDomain)
|
|
||||||
return event
|
|
||||||
|
|
||||||
if (`{{ .DevMode }}` == 'true')
|
if ({{ .DevMode }})
|
||||||
return event
|
return event
|
||||||
|
|
||||||
event.respondWith(fetchAsset(event))
|
event.respondWith(fetchAsset(event))
|
||||||
|
|
|
||||||
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
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
|
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
|
||||||
"node_store": "/js/{{ .VERSION }}/node_store.mjs",
|
"node_store": "/js/{{ .VERSION }}/node_store.mjs",
|
||||||
"node": "/js/{{ .VERSION }}/page_node.mjs",
|
"node": "/js/{{ .VERSION }}/page_node.mjs",
|
||||||
"sidebar": "/js/{{ .VERSION }}/sidebar.mjs"
|
"tree": "/js/{{ .VERSION }}/tree.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
{{ define "page" }}
|
{{ define "page" }}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
|
<div id="notes2">
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/page_history.css">
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Drag and drop elements -->
|
|
||||||
|
|
||||||
<!-- page-node -->
|
|
||||||
<div id="notes2" class="page-node">
|
|
||||||
<div id="tree-expander" onclick="window._mbus.dispatch('TREE_EXPANSION', { expand: true })">></div>
|
|
||||||
<div id="tree" tabindex=0></div>
|
<div id="tree" tabindex=0></div>
|
||||||
|
|
||||||
<div id="main-page">
|
<div id="main-page">
|
||||||
|
|
@ -16,40 +8,26 @@
|
||||||
<n2-pagestorage></n2-pagestorage>
|
<n2-pagestorage></n2-pagestorage>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="page-root">
|
|
||||||
<div>
|
|
||||||
<img src="/images/{{ .VERSION }}/logo.svg">
|
|
||||||
<div> {{ .VERSION }}</div>
|
|
||||||
|
|
||||||
<div class="create">Create note</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Node editing -->
|
<!-- Node editing -->
|
||||||
<div id="page-node">
|
<div id="page-node">
|
||||||
<div id="crumbs"></div>
|
<div id="crumbs"></div>
|
||||||
|
<n2-syncprogress></n2-syncprogress>
|
||||||
<n2-nodeui id="note"></n2-nodeui>
|
<n2-nodeui id="note"></n2-nodeui>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- History -->
|
|
||||||
<n2-pagehistory id="page-history"></n2-pagehistory>
|
|
||||||
|
|
||||||
<!-- Preferences -->
|
|
||||||
<n2-pagepreferences id="page-preferences"></n2-pagepreferences>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n2-syncprogress></n2-syncprogress>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|
||||||
|
|
||||||
import {NodeStore} from '/js/{{ .VERSION }}/node_store.mjs'
|
import {NodeStore} from '/js/{{ .VERSION }}/node_store.mjs'
|
||||||
|
|
||||||
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 }}/file.mjs'
|
import { } from '/js/{{ .VERSION }}/file.mjs'
|
||||||
|
|
||||||
window.Sync = Sync
|
window.Sync = Sync
|
||||||
|
|
|
||||||