diff --git a/authentication/pkg.go b/authentication/pkg.go index c0b9a2e..9eb6245 100644 --- a/authentication/pkg.go +++ b/authentication/pkg.go @@ -8,9 +8,6 @@ import ( "github.com/jmoiron/sqlx" "github.com/lib/pq" - // Internal - appUser "notes2/user" - // Standard "database/sql" "encoding/hex" @@ -30,6 +27,12 @@ type Manager struct { ExpireDays int } +type User struct { + ID int + Username string + Name string +} + func httpError(w http.ResponseWriter, err error) { // {{{ j, _ := json.Marshal(struct { OK bool @@ -162,16 +165,16 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques mngr.log.Info("authentication", "username", request.Username, "status", "accepted") j, _ := json.Marshal(struct { OK bool - User appUser.User + User User Token string }{true, user, token}) w.Write(j) } // }}} -func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user appUser.User, err error) { // {{{ +func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user User, err error) { // {{{ var row *sql.Row row = mngr.db.QueryRow(` - SELECT id, username, name, preferences + SELECT id, username, name FROM public.user WHERE LOWER(username) = LOWER($1) AND @@ -180,21 +183,13 @@ func (mngr *Manager) Authenticate(username, password string) (authenticated bool username, password, ) - var data []byte - err = row.Scan(&user.ID, &user.Username, &user.Name, &data) + err = row.Scan(&user.ID, &user.Username, &user.Name) if err != nil && err.Error() == "sql: no rows in result set" { err = nil authenticated = false return } if err != nil { - authenticated = false - return - } - - err = json.Unmarshal(data, &user.Preferences) - if err != nil { - authenticated = false return } @@ -283,7 +278,7 @@ func (mngr *Manager) ChangePassword(username, currentPassword, newPassword strin changed = (rowsAffected == 1) return } // }}} -func (mngr *Manager) NewClientUUID(user appUser.User) (clientUUID string, err error) { // {{{ +func (mngr *Manager) NewClientUUID(user User) (clientUUID string, err error) { // {{{ // Each client session has its own UUID. // Loop through until a unique one is established. var proposedClientUUID string diff --git a/main.go b/main.go index 6e3cf94..550b381 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( // Internal "notes2/authentication" "notes2/html_template" - appUser "notes2/user" "os" // Standard @@ -24,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v29" +const VERSION = "v24" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 @@ -135,8 +134,6 @@ func main() { // {{{ http.HandleFunc("/offline", pageOffline) http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) - http.HandleFunc("GET /user/preferences", authenticated(actionUserGetPreferences)) - http.HandleFunc("POST /user/preferences", authenticated(actionUserSetPreferences)) http.HandleFunc("/sync/from_server/count/{sequence}", authenticated(actionSyncFromServerCount)) http.HandleFunc("/sync/from_server/{sequence}/{offset}", authenticated(actionSyncFromServer)) @@ -181,7 +178,7 @@ func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.Respon } // User object is added to the context for the next handler. - user := appUser.NewUser(claims) + user := NewUser(claims) r = r.WithContext(context.WithValue(r.Context(), CONTEXT_USER, user)) Log.Debug("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username, "client", user.ClientUUID) @@ -269,7 +266,7 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{ // The purpose of the Client UUID is to avoid // sending nodes back once again to a client that // just created or modified it. - user := getUserSession(r) + user := getUser(r) changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) offset, _ := strconv.Atoi(r.PathValue("offset")) @@ -292,7 +289,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{ // The purpose of the Client UUID is to avoid // sending nodes back once again to a client that // just created or modified it. - user := getUserSession(r) + user := getUser(r) changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID) @@ -312,7 +309,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{ w.Write(j) } // }}} func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) + user := getUser(r) var err error uuid := r.PathValue("uuid") @@ -328,7 +325,7 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ }) } // }}} func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) + user := getUser(r) var err error uuid := r.PathValue("uuid") @@ -351,7 +348,7 @@ func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ }) } // }}} func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) + user := getUser(r) var err error uuid := r.PathValue("uuid") @@ -363,12 +360,12 @@ func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{ } responseData(w, map[string]any{ - "OK": true, - "Count": count, + "OK": true, + "Count": count, }) } // }}} func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) + user := getUser(r) body, _ := io.ReadAll(r.Body) var request struct { @@ -392,47 +389,6 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ }) } // }}} -func actionUserGetPreferences(w http.ResponseWriter, r *http.Request) { // {{{ - user := getUserSession(r) - prefs, err := user.Preferences() - if err != nil { - httpError(w, err) - return - } - - responseData(w, map[string]any{ - "OK": true, - "Preferences": prefs, - }) -} // }}} -func actionUserSetPreferences(w http.ResponseWriter, r *http.Request) { // {{{ - session := getUserSession(r) - - // Verify the "default" profile is still there. - var newPrefs map[string]appUser.UserPreferences - body, _ := io.ReadAll(r.Body) - err := json.Unmarshal(body, &newPrefs) - if err != nil { - httpError(w, err) - return - } - - if _, found := newPrefs["default"]; !found { - httpError(w, fmt.Errorf("'default' profile missing.")) - return - } - - err = session.SetPreferences(newPrefs) - if err != nil { - httpError(w, err) - return - } - - responseData(w, map[string]any{ - "OK": true, - }) -} // }}} - func createNewUser(username string) { // {{{ reader := bufio.NewReader(os.Stdin) @@ -475,8 +431,7 @@ func changePassword(username string) { // {{{ fmt.Printf("\nPassword changed\n") } // }}} -func getUserSession(r *http.Request) appUser.UserSession { // {{{ - user, _ := r.Context().Value(CONTEXT_USER).(appUser.UserSession) - user.Db = db +func getUser(r *http.Request) UserSession { // {{{ + user, _ := r.Context().Value(CONTEXT_USER).(UserSession) return user } // }}} diff --git a/node.go b/node.go index a25c771..e3a61d5 100644 --- a/node.go +++ b/node.go @@ -54,7 +54,6 @@ type Node struct { DeletedSeq sql.NullInt64 `db:"deleted_seq"` Content string ContentEncrypted string `db:"content_encrypted" json:"-"` - Special bool } func NodeTree(userID, offset int, synced uint64) (nodes []TreeNode, maxSeq uint64, moreRowsExist bool, err error) { // {{{ @@ -136,7 +135,6 @@ func Nodes(userID, offset int, synced uint64, clientUUID string) (nodes []Node, FROM public.node WHERE - NOT special AND user_id = $1 AND client != $5::uuid AND ( diff --git a/sql/00008.sql b/sql/00008.sql deleted file mode 100644 index 2701ba5..0000000 --- a/sql/00008.sql +++ /dev/null @@ -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$ -; diff --git a/sql/00009.sql b/sql/00009.sql deleted file mode 100644 index 50487f3..0000000 --- a/sql/00009.sql +++ /dev/null @@ -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(); diff --git a/sql/00010.sql b/sql/00010.sql deleted file mode 100644 index ecd8ab4..0000000 --- a/sql/00010.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."user" ADD preferences jsonb DEFAULT '{}' NOT NULL; diff --git a/static/css/markdown.css b/static/css/markdown.css index 832d4a2..1ecbc94 100644 --- a/static/css/markdown.css +++ b/static/css/markdown.css @@ -102,11 +102,6 @@ border: 1px solid #ccc; padding: 2px 4px; border-radius: 4px; - - &.copy { - border: var(--markdown-copy-border); - background-color: var(--markdown-copy-background); - } } pre { @@ -116,14 +111,6 @@ 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 { border: unset; padding: unset; diff --git a/static/css/notes2.css b/static/css/notes2.css index 7fdea0b..dfe4156 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -9,15 +9,7 @@ --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; + --functions-width: 216px; } html { @@ -28,10 +20,6 @@ html { filter: var(--colorize); } -textarea { - font-family: var(--font-monospace); -} - button { font-size: 1em; padding: 4px 8px; @@ -73,10 +61,9 @@ button { 1fr; } - /* The other pages just gets the whole page without dividing it up. */ - &:not(.page-node) { + &.page-history { grid-template-areas: - "tree-expander tree pad1 n2-page pad2" + "tree-expander tree pad1 n2-pagehistory pad2" ; grid-template-columns: @@ -199,11 +186,6 @@ button { img { width: auto; height: 18px; - - &.deleted { - height: 24px; - transform: translateX(3px) translateY(3px); - } } } @@ -246,6 +228,7 @@ button { #notes2 { &.page-node { + #page-root { display: none; } @@ -260,7 +243,7 @@ button { display: contents; n2-pagestorage { - grid-area: n2-page; + grid-area: content; } } } @@ -268,14 +251,9 @@ button { &.page-history { #page-history { display: grid; - grid-area: n2-page; - } - } + grid-area: n2-pagehistory; - &.page-preferences { - #page-preferences { - display: block; - grid-area: n2-page; + n2-pagehistory {} } } @@ -287,6 +265,7 @@ button { #page-root { display: contents !important; } + } } @@ -454,6 +433,7 @@ n2-nodeui { grid-area: content; justify-self: center; word-wrap: break-word; + font-family: monospace; font-size: 1em; color: #333; @@ -477,10 +457,6 @@ n2-nodeui { grid-area: content; display: none; - font-family: var(--font-monospace); - font-size: 1em; - font-weight: 400; - border-top: 1px solid #e0e0e0; margin-top: 8px; margin-bottom: 32px; diff --git a/static/images/icon_menu.svg b/static/images/icon_menu.svg index cfdd1e8..e60bdee 100644 --- a/static/images/icon_menu.svg +++ b/static/images/icon_menu.svg @@ -2,18 +2,18 @@ + transform="translate(-146.57917,-92.339583)"> +
`
+ return ``
+ (token.escaped ? code : escapeHtmlEntities(code, true))
+ '
\n'
}
- return `'
+ (token.escaped ? code : escapeHtmlEntities(code, true))
@@ -285,7 +260,7 @@ export class MarkedPosition {
},
codespan(token) {
- return `${escapeHtmlEntities(token.text, true)}`
+ return `${escapeHtmlEntities(token.text, true)}`
},
br(token) {
diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs
index 6be8f82..324f004 100644
--- a/static/js/node_store.mjs
+++ b/static/js/node_store.mjs
@@ -1,8 +1,6 @@
import { Node } from 'node'
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 {
constructor() {//{{{
@@ -15,8 +13,6 @@ export class NodeStore {
this.sendQueue = null
this.nodesHistory = null
this.files = null
-
- this.initializeSpecialNodes()
}//}}}
initializeDB() {//{{{
return new Promise((resolve, reject) => {
@@ -80,7 +76,8 @@ export class NodeStore {
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
this.nodesHistory = new NodeHistoryStore(this.db, 'nodes_history')
this.files = new SimpleNodeStore(this.db, 'files')
- resolve()
+ this.initializeRootNode()
+ .then(() => resolve())
}
req.onerror = (event) => {
@@ -88,11 +85,40 @@ export class NodeStore {
}
})
}//}}}
- initializeSpecialNodes() {// {{{
- this.nodes[ROOT_NODE] = new Node({ UUID: ROOT_NODE, Name: 'Start', Special: true }, -1)
- this.nodes[DELETED_NODE] = new Node({ UUID: DELETED_NODE, Name: 'Deleted nodes', Special: true }, -1)
- this.nodes[ORPHANED_NODE] = new Node({ UUID: ORPHANED_NODE, Name: 'Orphaned nodes', Special: true }, -1)
- }// }}}
+ initializeRootNode() {//{{{
+ return new Promise((resolve, reject) => {
+ // The root node is a magical node which displays as the first node if none is specified.
+ // 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) {//{{{
let n = this.nodes[uuid]
@@ -246,14 +272,6 @@ export class NodeStore {
}//}}}
get(uuid, suppliedNodestore) {//{{{
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
// avoid creating new transactions.
let trx
@@ -291,16 +309,6 @@ export class NodeStore {
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)
getRequest.onsuccess = (event) => {
// Node not found in IndexedDB.
diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs
index 2106ada..f6aa681 100644
--- a/static/js/page_node.mjs
+++ b/static/js/page_node.mjs
@@ -2,70 +2,14 @@ import { ROOT_NODE, uuidv7 } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { MarkedPosition } from './marked_position.mjs'
-class N2NodeMenu extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
- `
- }// }}}
- constructor() {// {{{
- super()
- }// }}}
-}
-customElements.define('n2-nodemenu', N2NodeMenu)
-
export class N2PageNodeUI extends CustomHTMLElement {
static {// {{{
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
@@ -92,13 +28,11 @@ export class N2PageNodeUI extends CustomHTMLElement {
+
+
-
-
+
-
`
}// }}}
@@ -107,14 +41,12 @@ export class N2PageNodeUI extends CustomHTMLElement {
this.node = null
this.style.display = 'contents'
+ this.classList.add('show-markdown') // TODO Should probably be moved to settings.
this.marked = new MarkedPosition()
_mbus.subscribe('NODE_UI_OPEN', event => {
this.node = event.detail.data
-
-
- if (!this.node.isSpecial())
- this.showMarkdown(true)
+ this.showMarkdown(true)
this.render()
})
@@ -135,27 +67,11 @@ export class N2PageNodeUI extends CustomHTMLElement {
_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', async () => this.renameNode())
-
- // Bind handlers for content keyboard input and paste.
this.elNodeContent.addEventListener('input', event => this.contentChanged(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.elIconNewDocument.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()
-
+ this.elIconTableFormat.addEventListener('click', event => {
if (!event.shiftKey)
this.elNodeContent.value = this.formatAllTables(this.elNodeContent.value)
else {
@@ -169,12 +85,15 @@ export class N2PageNodeUI extends CustomHTMLElement {
this.node.setContent(this.elNodeContent.value)
})
- this.elNodeMenu.elHistory.addEventListener('click', () => {
- _mbus.dispatch('SHOW_PAGE', { page: 'history' })
+ this.elIconHistory.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'history' }))
+ this.elIconSave.addEventListener('click', () => this.saveNode())
+ this.elIconNewDocument.addEventListener('click', event => {
+ if (event.shiftKey)
+ _app.createNode(this.node.ParentUUID)
+ else
+ _app.createNode()
})
- // Default is to always show markdown.
- this.classList.add('show-markdown') // TODO Should probably be moved to settings.
this.showMarkdown(true)
}// }}}
renderName() {// {{{
@@ -387,7 +306,7 @@ export class N2PageNodeUI extends CustomHTMLElement {
// 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 newValue =`[${checkbox.checked ? 'x' : ' '}] `
const modifiedContent = this.node.content().slice(0, pos.start) + newValue + this.node.content().slice(pos.end)
this.node.setContent(modifiedContent)
@@ -505,9 +424,6 @@ export class Node {
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
@@ -518,9 +434,6 @@ export class Node {
isFirstSibling() {//{{{
return this._sibling_before === null
}//}}}
- isSpecial() {// {{{
- return this.data.Special
- }// }}}
content() {//{{{
/* TODO - implement crypto
if (this.CryptoKeyID != 0 && !this._decrypted)
@@ -584,10 +497,23 @@ export class Node {
console.log('waiting')
await Promise.all([history, sendQueue, nodeStoreAdding])
console.log('waiting done')
-
+
return
}//}}}
}
+class N2Menu extends CustomHTMLElement {
+ static {
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+
+ `
+ }
+
+ constructor() {
+ super()
+ }
+}
+customElements.define('n2-menu', N2Menu)
// vim: foldmethod=marker
diff --git a/static/js/page_preferences.mjs b/static/js/page_preferences.mjs
deleted file mode 100644
index 9655278..0000000
--- a/static/js/page_preferences.mjs
+++ /dev/null
@@ -1,283 +0,0 @@
-import { CustomHTMLElement } from "./lib/custom_html_element.mjs"
-import { API } from './api.mjs'
-
-export class N2PagePreferences extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
- Preferences
-
- Changes preferences to not download images or files on the device doesn't remove the already downloaded data.
-
-
- Device preference set
-
-
-
-
-
-
-
- `
- }// }}}
- constructor() {// {{{
- super(true)
- this.sets = []
-
- this.elNewSet.addEventListener('click', () => this.newSet())
- this.elSave.addEventListener('click', () => this.save())
- this.elDevPreferenceSet.addEventListener('change', event=>this.changePreferenceSet(event))
-
- window._mbus.subscribe('SHOW_PAGE', async event => {
- if (event.detail.data?.page == 'preferences') {
- this.sets = await this.getPreferenceSets()
- this.render()
- }
- })
-
- window._mbus.subscribe('PREFERENCE_SET_MODIFIED', () => this.preferencesModified())
- window._mbus.subscribe('PREFERENCE_SET_DELETE', event => this.preferencesDelete(event.detail.data.set))
- }// }}}
- sortSets(a, b) {// {{{
- if (a.name == 'default') return -1
- if (b.name == 'default') return 1
-
- if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
- if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
-
- return 0
- }// }}}
- async render() {// {{{
- try {
- this.sets.sort(this.sortSets)
- this.elSets.replaceChildren(...this.sets)
-
- const setNames = this.sets.entries().map(([i, set]) => {
- const optn = document.createElement('option')
- optn.innerText = set.name
- return optn
- })
- this.elDevPreferenceSet.replaceChildren(...setNames)
- } catch (e) {
- console.error(e)
- alert(e.message)
- }
- }// }}}
- async getPreferenceSets() {// {{{
- const userData = localStorage.getItem('user')
- if (userData === null)
- throw new Error('Could not find user in localStorage')
-
- const user = JSON.parse(userData)
- const prefsData = user.Preferences
-
- if (prefsData === undefined)
- throw new Error('User object is missing preferences')
-
- if (!prefsData.hasOwnProperty('default'))
- throw new Error('The "default" preferences set is missing')
-
- return Object.keys(prefsData).map(name => new N2PreferenceSet(name, prefsData[name]))
- }// }}}
- async retrieveServerPreferences() {// {{{
- try {
- API.query('GET', '/user/preferences')
- } catch (e) {
- console.error(e)
- alert(`Error retrieving preferences: ${e.message}`)
- }
- }// }}}
- changePreferenceSet(event) {// {{{
- this.preferencesModified()
- }// }}}
- newSet() {// {{{
- let name = prompt("Name for new preference set")
- if (!name)
- return
-
- name = name.trim()
- if (name === '')
- return
-
- if (name == 'default') {
- alert(`Name can't be "default".`)
- return
- }
-
- const exists = this.sets.some(s => s.name.toLowerCase() == name.toLowerCase())
- if (exists) {
- alert(`Set with name "${name}" already exist.`)
- return
- }
-
- this.sets.push(new N2PreferenceSet(name, {}))
- this.preferencesModified()
- this.render()
- }// }}}
- preferencesModified() {// {{{
- this.elSave.removeAttribute('disabled')
- }// }}}
- preferencesDelete(deleteSet) {// {{{
- if (deleteSet.name == 'default') {
- alert("Can't delete the default set.")
- return
- }
-
- if (!confirm(`Confirm deleting "${deleteSet.name}"`))
- return
-
- this.sets = this.sets.filter(set => {
- return !(set.name === deleteSet.name)
- })
-
- this.preferencesModified()
- this.render()
- }// }}}
- async save() {// {{{
- try {
- let newPrefs = {}
- this.sets.forEach(s => {
- const setState = s.getState()
- newPrefs[setState.name] = setState.state
- })
-
- // Throws exception on both HTTP and application errors.
- await API.query('POST', '/user/preferences', newPrefs)
-
- const userData = localStorage.getItem('user')
- const user = JSON.parse(userData)
- user.Preferences = newPrefs
- localStorage.setItem('user', JSON.stringify(user))
- localStorage.setItem('device_preference_set', this.elDevPreferenceSet.value)
- _mbus.dispatch('DEVICE_PREFERENCE_SET_UPDATED')
- } catch (e) {
- console.error(e)
- alert(e.message)
- } finally {
- this.elSave.setAttribute('disabled', true)
- }
-
- }// }}}
-}
-customElements.define('n2-pagepreferences', N2PagePreferences)
-
-// Preferences is a set of preferences, of which there can be many named.
-export class N2PreferenceSet extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
-
-
- ✘
-
-
-
-
-
-
-
- `
- }// }}}
- constructor(name, data) {// {{{
- super(true)
- this.name = name
- this.data = data
- this.render()
-
- // Enable the save button when settings are modified.
- this.allFields().forEach(f =>
- f.addEventListener('input', () => _mbus.dispatch('PREFERENCE_SET_MODIFIED'))
- )
-
- this.elName.addEventListener('click', () => this.updateName())
- this.elDelete.addEventListener('click', () => this.deleteSet())
- }// }}}
- updateName() {// {{{
- if (this.name == 'default') {
- alert('Can not change name of the default profile.')
- return
- }
-
- const name = prompt("Change name", this.name)
- if (!name)
- return
-
- this.name = name
- this.render()
- _mbus.dispatch('PREFERENCE_SET_MODIFIED')
- }// }}}
- deleteSet() {// {{{
- _mbus.dispatch('PREFERENCE_SET_DELETE', { set: this })
- }// }}}
- render() {// {{{
- this.elName.innerText = this.name
-
- this.fieldDownloadImages.checked = this.data.DownloadImages
- this.fieldDownloadFiles.checked = this.data.DownloadFiles
- }// }}}
- getState() {// {{{
- const name = this.name.trim()
- if (name === '')
- throw new Error('Name can not be empty.')
-
- return {
- name: this.name.trim(),
- state: this.fieldValues(),
- }
- }// }}}
-}
-customElements.define('n2-preferenceset', N2PreferenceSet)
diff --git a/static/js/page_storage.mjs b/static/js/page_storage.mjs
index a007130..931a718 100644
--- a/static/js/page_storage.mjs
+++ b/static/js/page_storage.mjs
@@ -13,10 +13,7 @@ export class N2PageStorage extends CustomHTMLElement {
constructor() {
super()
- window._mbus.subscribe('SHOW_PAGE', event => {
- if (event.detail.data?.page == 'storage')
- this.render()
- })
+ window._mbus.subscribe('SHOW_PAGE', () => this.render())
}
async render() {
const countNodes = await globalThis.nodeStore.nodeCount()
diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs
index 6cd5814..63c4ddc 100644
--- a/static/js/sidebar.mjs
+++ b/static/js/sidebar.mjs
@@ -1,5 +1,4 @@
-import { ROOT_NODE, ORPHANED_NODE, DELETED_NODE } from 'node_store'
-import { Node } from 'node'
+import { ROOT_NODE } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { Color, Solver } from './lib/css_colorize.mjs'
@@ -128,7 +127,6 @@ export class N2Sidebar extends CustomHTMLElement {
this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search'))
this.elSync.addEventListener('click', () => _sync.run())
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
- this.elSettings.addEventListener('click', ()=> _mbus.dispatch('SHOW_PAGE', { page: 'preferences' }))
this.elHideTree.addEventListener('click', event => {
event.stopPropagation()
_mbus.dispatch('TREE_EXPANSION', { expand: false })
@@ -158,26 +156,8 @@ export class N2Sidebar extends CustomHTMLElement {
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
@@ -198,8 +178,9 @@ export class N2Sidebar extends CustomHTMLElement {
this.expandedNodes[UUID] = false
return this.expandedNodes[UUID]
}//}}}
- async setNodeExpanded(node, value) {//{{{
+ setNodeExpanded(node, value) {//{{{
let expanded = this.expandedNodes[node.UUID]
+
if (expanded === undefined) {
this.expandedNodes[node.UUID] = false
expanded = false
@@ -249,6 +230,8 @@ export class N2Sidebar extends CustomHTMLElement {
// Holding shift down does it recursively.
case Space:
case 'Enter':
+ if (n.UUID === ROOT_NODE)
+ return
const expanded = this.getNodeExpanded(n.UUID)
if (event.shiftKey) {
this.recursiveExpand(n, !expanded)
@@ -257,31 +240,38 @@ export class N2Sidebar extends CustomHTMLElement {
}
break
+ case 'g':
case 'Home':
this.navigateTop()
break
+ case 'G':
case 'End':
this.navigateBottom()
break
+ case 'j':
case 'ArrowDown':
await this.navigateDown(this.selectedNode)
break
+ case 'k':
case 'ArrowUp':
await this.navigateUp(this.selectedNode)
break
+ case 'h':
case 'ArrowLeft':
await this.navigateLeft(this.selectedNode)
break
+ case 'l':
case 'ArrowRight':
await this.navigateRight(this.selectedNode)
break
default:
+ // nonsole.log(event.key)
handled = false
}
@@ -403,26 +393,23 @@ export class N2Sidebar extends CustomHTMLElement {
}//}}}
async navigateTop() {//{{{
const root = await nodeStore.get(ROOT_NODE)
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.UUID, dontPush: false, dontExpand: true })
+ if (root.Children.length === 0)
+ return
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: false, dontExpand: true })
}//}}}
async navigateBottom() {//{{{
- const orphaned = await nodeStore.get(ORPHANED_NODE)
-
- if (!orphaned.hasChildren() || this.getNodeExpanded(orphaned.UUID)) {
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.UUID, dontPush: false, dontExpand: true })
+ const root = await nodeStore.get(ROOT_NODE)
+ if (root.Children.length === 0)
return
- }
- /* TODO - fix this when orphaned nodes are implemented.
- const toplevel = orphaned.Children[orphaned.Children.length - 1]
+ const toplevel = root.Children[root.Children.length - 1]
const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID)
if (toplevelExpanded) {
const lastnode = this.getLastExpandedNode(toplevel)
_mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: false, dontExpand: true })
} else
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.Children[orphaned.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
- */
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
}//}}}
getParentWithNextSibling(node) {//{{{
@@ -443,10 +430,6 @@ export class N2Sidebar extends CustomHTMLElement {
if (state)
await this.setNodeExpanded(node, true)
- // An expanded node needs to have its children fetched.
- if (!node.hasFetchedChildren())
- await node.fetchChildren()
-
for (const child of node.Children)
await this.recursiveExpand(child, state)
@@ -479,9 +462,7 @@ export class N2TreeNode extends CustomHTMLElement {
this.tmpl.innerHTML = `