diff --git a/main.go b/main.go index 5bfdc84..10d578b 100644 --- a/main.go +++ b/main.go @@ -140,8 +140,6 @@ 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) @@ -330,46 +328,6 @@ 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 0beb1b1..ffcc89f 100644 --- a/node.go +++ b/node.go @@ -3,8 +3,8 @@ package main import ( // External werr "git.gibonuddevalla.se/go/wrappederror" - "github.com/derektata/lorem/ipsum" "github.com/jmoiron/sqlx" + "github.com/derektata/lorem/ipsum" // Standard "database/sql" @@ -248,71 +248,6 @@ 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 28ee7a2..a41afda 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -5,6 +5,9 @@ --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; @@ -20,11 +23,6 @@ html { filter: var(--colorize); } -button { - font-size: 1em; - padding: 4px 8px; -} - /* ------------------------------------- * * Default application grid in wide mode * * ------------------------------------- */ @@ -521,137 +519,52 @@ dialog.op { n2-pagehistory { - - .back, - .node-name { + .layout { display: grid; grid-template-columns: min-content 1fr; grid-gap: 8px; align-items: center; - margin-bottom: 16px; - } - .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-back-image, + .el-back-text { + cursor: pointer; + } - .group { - border: 1px solid #ccc; - padding: 32px; - margin-bottom: 32px; - border-radius: 8px; - background-color: #fafafa; + .el-node-name { + margin-left: 8px; + } - 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-nodes { + grid-column: 1 / -1; - .el-stats { - margin-bottom: 16px; - display: grid; - grid-template-columns: min-content 1fr; - grid-gap: 8px 12px; - white-space: nowrap; - } + display: grid; + grid-template-columns: min-content 1fr; + grid-gap: 4px 8px; - .el-fetch-history-progress { - margin-top: 16px; - } + .history-node { + display: contents; + } + } - .el-back-image, - .el-back-text { - cursor: pointer; - } + .pagination { + grid-column: 1 / -1; + margin-top: 16px; - .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; + display: grid; + grid-template-columns: repeat(3, min-content); + grid-gap: 32px; white-space: nowrap; - } + user-select: none; - n2-pagehistorynode { - - &.selected .el-index:after { - position: absolute; - left: -20px; - - content: '>'; - color: var(--color1); + .el-prev { font-weight: bold; - margin-right: 8px; + cursor: pointer; } - .el-index { - position: relative; - text-align: right; - } - - .el-updated { - white-space: initial; - } - - .el-date { - white-space: nowrap; + .el-next { 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); + cursor: pointer; } } } } - -.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 9e8a672..d3b95de 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -456,30 +456,13 @@ class NodeHistoryStore extends SimpleNodeStore { 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) {// {{{ + 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, 'prev') + .openCursor(uuid) let retrieved = 0 let first = true @@ -514,10 +497,10 @@ class NodeHistoryStore extends SimpleNodeStore { } } }) - }// }}} + } } -export function uuidv7() {// {{{ +export function uuidv7() { // random bytes const value = new Uint8Array(16) crypto.getRandomValues(value) @@ -541,6 +524,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 37404c7..434f47b 100644 --- a/static/js/page_history.mjs +++ b/static/js/page_history.mjs @@ -1,299 +1,94 @@ 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 {// {{{ + static PAGESIZE = 10 + + static { this.tmpl = document.createElement('template') this.tmpl.innerHTML = ` -
+
Back to node
-
-

-
- -
Actions
-
- -
-
- - -
History
-
-
-
History on server:
-
- -
History on client:
-
-
-
+ +
- -
` - }// }}} + } - constructor() {// {{{ + 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) - - }) + this.elPrev.addEventListener('click', ()=>this.prevPage()) + this.elNext.addEventListener('click', ()=>this.nextPage()) _mbus.subscribe('NODE_UI_OPEN', async (event) => { await this.useNode(event.detail.data) this.render() }) + } - _mbus.subscribe('HISTORY_NODE_SELECTED', (event) => { - this.selectedNode = event.detail.data.historyNode - }) - }// }}} - 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() - } - - // Any selected history node is rendered with markdown. - /* - this.marked = new MarkedPosition() - this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value) - */ - }// }}} - - async useNode(node) {// {{{ + 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) {// {{{ - 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 - } - }// }}} + } - prevPage() {// {{{ + + prevPage() { if (this.page == 1) return - - // Selecting a node on another page is wrong. - this.selectedNode = null this.page-- this.render() - }// }}} - nextPage() {// {{{ + } + + 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() + async render() { + this.elNodeName.innerText = this.node.get('Name') + this.elPage.innerText = `${this.page} / ${this.pages}` - if (!json.OK) { - alert(json.Error) - return + this.elNodes.innerHTML = '' + let nodes = await nodeStore.nodesHistory.retrievePage(this.node.UUID, N2PageHistory.PAGESIZE, this.page) + let i = 0 + for (const n of nodes) { + i++ + const div = document.createElement('div') + div.innerHTML = ` +
${N2PageHistory.PAGESIZE * (this.page - 1) + i}
+
${n.get('Updated').replace('T', ' ')}
+ ` + div.classList.add('history-node') + this.elNodes.append(div) } - - 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 = ` -
-
-
-
- ` - }// }}} - 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.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)