diff --git a/main.go b/main.go index 10d578b..5bfdc84 100644 --- a/main.go +++ b/main.go @@ -140,6 +140,8 @@ func main() { // {{{ http.HandleFunc("/sync/to_server", authenticated(actionSyncToServer)) 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) @@ -328,6 +330,46 @@ func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ "Node": node, }) } // }}} +func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ + user := getUser(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 := getUser(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) { // {{{ user := getUser(r) diff --git a/node.go b/node.go index ffcc89f..0beb1b1 100644 --- a/node.go +++ b/node.go @@ -3,8 +3,8 @@ package main import ( // External werr "git.gibonuddevalla.se/go/wrappederror" - "github.com/jmoiron/sqlx" "github.com/derektata/lorem/ipsum" + "github.com/jmoiron/sqlx" // Standard "database/sql" @@ -248,6 +248,71 @@ func RetrieveNode(userID int, nodeUUID string) (node Node, err error) { // {{{ 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, + 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) { // {{{ var rows *sqlx.Rows rows, err = db.Queryx(` diff --git a/static/css/notes2.css b/static/css/notes2.css index a41afda..28ee7a2 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -5,9 +5,6 @@ --thumbnail-width: 300px; --thumbnail-height: 100px; - /* - --colorize: invert(10%) sepia(61%) saturate(5017%) hue-rotate(323deg) brightness(90%) contrast(109%); - */ --colorize: invert(59%) sepia(71%) saturate(3270%) hue-rotate(327deg) brightness(100%) contrast(99%); --line-color: #ccc; @@ -23,6 +20,11 @@ html { filter: var(--colorize); } +button { + font-size: 1em; + padding: 4px 8px; +} + /* ------------------------------------- * * Default application grid in wide mode * * ------------------------------------- */ @@ -519,52 +521,137 @@ dialog.op { n2-pagehistory { - .layout { + + .back, + .node-name { display: grid; grid-template-columns: min-content 1fr; grid-gap: 8px; align-items: center; + margin-bottom: 16px; + } - .el-back-image, - .el-back-text { - cursor: pointer; - } + .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; + } - .el-node-name { - margin-left: 8px; - } + .group { + border: 1px solid #ccc; + padding: 32px; + margin-bottom: 32px; + border-radius: 8px; + background-color: #fafafa; - .el-nodes { - grid-column: 1 / -1; + 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; + } - display: grid; - grid-template-columns: min-content 1fr; - grid-gap: 4px 8px; + .el-stats { + margin-bottom: 16px; + display: grid; + grid-template-columns: min-content 1fr; + grid-gap: 8px 12px; + white-space: nowrap; + } - .history-node { - display: contents; - } - } + .el-fetch-history-progress { + margin-top: 16px; + } - .pagination { - grid-column: 1 / -1; - margin-top: 16px; + .el-back-image, + .el-back-text { + cursor: pointer; + } - display: grid; - grid-template-columns: repeat(3, min-content); - grid-gap: 32px; + .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; - user-select: none; + } - .el-prev { + n2-pagehistorynode { + + &.selected .el-index:after { + position: absolute; + left: -20px; + + content: '>'; + color: var(--color1); font-weight: bold; - cursor: pointer; + margin-right: 8px; } - .el-next { + .el-index { + position: relative; + text-align: right; + } + + .el-updated { + white-space: initial; + } + + .el-date { + white-space: nowrap; font-weight: bold; - cursor: pointer; + } + + .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; + } +} +} diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index d3b95de..9e8a672 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -456,13 +456,30 @@ class NodeHistoryStore extends SimpleNodeStore { request.onerror = (event) => reject(event.target.error) }) }//}}} - retrievePage(uuid, perPage, page) { + 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 cursor = this.db .transaction(['nodes', this.storeName], 'readonly') .objectStore(this.storeName) .index('byUUID') - .openCursor(uuid) + .openCursor(uuid, 'prev') let retrieved = 0 let first = true @@ -497,10 +514,10 @@ class NodeHistoryStore extends SimpleNodeStore { } } }) - } + }// }}} } -export function uuidv7() { +export function uuidv7() {// {{{ // random bytes const value = new Uint8Array(16) crypto.getRandomValues(value) @@ -524,6 +541,6 @@ export function uuidv7() { .map((b) => b.toString(16).padStart(2, "0")) .join("") return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}` -} +}// }}} // vim: foldmethod=marker diff --git a/static/js/page_history.mjs b/static/js/page_history.mjs index 434f47b..37404c7 100644 --- a/static/js/page_history.mjs +++ b/static/js/page_history.mjs @@ -1,94 +1,299 @@ 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 = 10 - - static { + static PAGESIZE = 15 + static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = ` -