diff --git a/main.go b/main.go index 10d578b..22facee 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,7 @@ import ( const VERSION = "v11" const CONTEXT_USER = 1 -const SYNC_PAGINATION = 200 +const SYNC_PAGINATION = 20 var ( FlagGenerate bool @@ -140,6 +140,7 @@ 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("/service_worker.js", pageServiceWorker) @@ -328,6 +329,29 @@ 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 actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ user := getUser(r) diff --git a/node.go b/node.go index ffcc89f..4d7da95 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,51 @@ 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 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 05fa6f2..11b8164 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -516,87 +516,115 @@ 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: #f8f8f8; - .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 minmax(min-content, max-content) min-content 1fr; + .el-back-image, + .el-back-text { + cursor: pointer; + } - background-color: var(--line-color); - gap: 1px; - border: 1px solid var(--line-color); + .el-node-name { + margin-left: 8px; + } - &>div>div { - padding: 8px 12px; - background-color: #fff; - white-space: nowrap; + .el-nodes { + grid-column: 1 / -1; - &.index { - text-align: right; - } + display: grid; + grid-template-columns: min-content minmax(min-content, max-content) min-content 1fr; - &.updated { - white-space: initial; - } + background-color: var(--line-color); + gap: 1px; + border: 1px solid var(--line-color); - .date { - white-space: nowrap; - font-weight: bold; - } - - .time { - white-space: nowrap; - color: #555; - } - - &.name { - white-space: initial; - /*overflow-wrap: anywhere;*/ - word-break: break-all; - color: var(--color1); - - } - } - - .history-node { - display: contents; - } - } - - .pagination { - grid-column: 1 / -1; - margin-top: 16px; - - display: grid; - grid-template-columns: repeat(3, min-content); - grid-gap: 32px; + &>div>div { + padding: 8px 12px; + background-color: #fff; white-space: nowrap; - user-select: none; - .el-prev { - font-weight: bold; - cursor: pointer; + &.index { + text-align: right; } - .el-next { - font-weight: bold; - cursor: pointer; + &.updated { + white-space: initial; } + + .date { + white-space: nowrap; + font-weight: bold; + } + + .time { + white-space: nowrap; + color: #555; + } + + &.name { + white-space: initial; + /*overflow-wrap: anywhere;*/ + word-break: break-all; + color: var(--color1); + + } + } + + .history-node { + display: contents; + } + } + + .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 033e5eb..9e8a672 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -456,7 +456,24 @@ 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') @@ -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 ee662df..9149a02 100644 --- a/static/js/page_history.mjs +++ b/static/js/page_history.mjs @@ -1,7 +1,8 @@ import { CustomHTMLElement } from './lib/custom_html_element.mjs' +import { Node } from './page_node.mjs' export class N2PageHistory extends CustomHTMLElement { - static PAGESIZE = 25 + static PAGESIZE = 15 static { this.tmpl = document.createElement('template') @@ -9,22 +10,30 @@ export class N2PageHistory extends CustomHTMLElement { -