diff --git a/html_template/pkg.go b/html_template/pkg.go
index 4140f89..9abfc12 100644
--- a/html_template/pkg.go
+++ b/html_template/pkg.go
@@ -66,8 +66,6 @@ func (e *Engine) ReloadTemplates() { // {{{
} // }}}
func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{
- var err error
-
// URLs with pattern /(css|images)/v1.0.0/foobar are stripped of the version.
// To get rid of problems with cached content in browser on a new version release,
// while also not disabling cache altogether.
@@ -83,11 +81,7 @@ func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{
r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2])
if e.DevMode {
- p := fmt.Sprintf("static/%s/%s", comp[1], comp[2])
- _, err = os.Stat(p)
- if err == nil {
- e.staticLocalFS.ServeHTTP(w, r)
- }
+ e.staticLocalFS.ServeHTTP(w, r)
return
}
}
diff --git a/static/css/notes2.css b/static/css/notes2.css
index 8351858..31e1f1f 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -145,6 +145,29 @@ html {
}
}
+[id^="page-"] {
+ display: none;
+}
+
+#main-page {
+ display: contents;
+
+ &.node {
+ #page-node {
+ display: contents;
+ }
+ }
+
+ &.storage {
+ #page-storage {
+ display: contents;
+ n2-pagestorage {
+ grid-area: content;
+ }
+ }
+ }
+}
+
#crumbs {
grid-area: crumbs;
display: grid;
diff --git a/static/images/file_icons/application_pdf.svg b/static/images/file_icons/application_pdf.svg
new file mode 100644
index 0000000..e503d4d
--- /dev/null
+++ b/static/images/file_icons/application_pdf.svg
@@ -0,0 +1,107 @@
+
+
+
+
diff --git a/static/images/file_icons/generic.svg b/static/images/file_icons/generic.svg
new file mode 100644
index 0000000..7ca6c2b
--- /dev/null
+++ b/static/images/file_icons/generic.svg
@@ -0,0 +1,100 @@
+
+
+
+
diff --git a/static/js/app.mjs b/static/js/app.mjs
index dc60b59..feeec3a 100644
--- a/static/js/app.mjs
+++ b/static/js/app.mjs
@@ -29,6 +29,14 @@ export class App {
this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
})
+ _mbus.subscribe('SHOW_PAGE', ({ detail: { data: { page } } }) => {
+ const classList = document.querySelector('#main-page').classList
+ classList.forEach(e =>
+ classList.remove(e)
+ )
+ classList.add(page)
+ })
+
window.addEventListener('keydown', event => this.keyHandler(event))
window.addEventListener('popstate', event => this.popState(event))
document.getElementById('notes2').addEventListener('click', event => {
@@ -36,6 +44,8 @@ export class App {
document.getElementById('node-content')?.focus()
})
+ _mbus.dispatch('SHOW_PAGE', { page: 'node' })
+
window._sync = new Sync()
// I think it is uncomfortable having the sync running as soon as the page load.
diff --git a/static/js/file.mjs b/static/js/file.mjs
new file mode 100644
index 0000000..2674737
--- /dev/null
+++ b/static/js/file.mjs
@@ -0,0 +1,81 @@
+import { CustomHTMLElement } from "./lib/custom_html_element.mjs";
+
+export class N2File extends CustomHTMLElement {
+ static {
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+
+
+
+ `
+ }
+ constructor() {
+ super(true)
+
+ this.addEventListener('click', event => {
+ event.preventDefault()
+ event.stopPropagation()
+
+ window.open(
+ URL.createObjectURL(this.file),
+ (event.ctrlKey || event.shiftKey) ? '_blank' : '_self',
+ )
+ })
+
+ this.render()
+ }
+
+ async render() {
+ const src = this.getAttribute('src')
+
+ // N2's db:// URLs are fetched from IndexedDB.
+ if (src.toLowerCase().match('^db://[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')) {
+ // image population has to happen asynchronously,
+ // while Marked lib has to be returned a string when exiting this function.
+ // populateImg makes sure this returned img element exists and then populates it
+ // with the image from IndexedDB.
+ const file = await globalThis.nodeStore.files.get(src.slice(5))
+ if (!file)
+ return
+ this.file = file.file
+
+ if (file.file.type.startsWith('image/'))
+ this.elImage.src = URL.createObjectURL(file.file)
+ else {
+ // Check for and use an existing MIME type icon.
+ // Place them in static/images/file_icons/ and replace the slash with an underscore.
+ const url = `/images/${_VERSION}/file_icons/${file.file.type.replaceAll('/', '_')}.svg`
+ const res = await fetch(url)
+ if (res.ok)
+ this.elImage.src = url
+
+ this.elFilename.innerText = file.file.name
+ this.elFilename.style.display = 'block'
+ }
+ } else
+ this.elImage.src = src
+ }
+}
+customElements.define('n2-file', N2File)
diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs
index dedb5d8..2cec808 100644
--- a/static/js/lib/custom_html_element.mjs
+++ b/static/js/lib/custom_html_element.mjs
@@ -1,10 +1,10 @@
export class CustomHTMLElement extends HTMLElement {
- constructor() {// {{{
+ constructor(useShadow) {// {{{
super()
- this.appendChild(this.constructor.tmpl.content.cloneNode(true))
-
- this.querySelectorAll('*').forEach(el => {
+ const workOn = useShadow ? this.attachShadow({ mode: 'open' }) : this
+ workOn.appendChild(this.constructor.tmpl.content.cloneNode(true))
+ workOn.querySelectorAll('*').forEach(el => {
const field = el.dataset.field
if (field !== undefined) {
const fieldName = this.toElementName('field', field)
diff --git a/static/js/marked_position.mjs b/static/js/marked_position.mjs
index 5c9c0ff..62a6996 100644
--- a/static/js/marked_position.mjs
+++ b/static/js/marked_position.mjs
@@ -92,18 +92,22 @@ function escapeHtmlEntities(html, encode) {// {{{
export class MarkedPosition {
constructor() {// {{{
- window.setpos = (event) => {
- event.stopPropagation()
- event.preventDefault()
-
- _mbus.dispatch('MARKDOWN_EDIT', {
- position: {
- start: event.target.dataset.offsetStart,
- end: event.target.dataset.offsetEnd,
- }
- })
- }
+ window.setpos = (event) => this.setpos(event)
+ this.render()
+ }// }}}
+ setpos(event) {// {{{
+ event.stopPropagation()
+ event.preventDefault()
+ _mbus.dispatch('MARKDOWN_EDIT', {
+ position: {
+ start: event.target.closest('[data-offset-start]').dataset.offsetStart,
+ end: event.target.closest('[data-offset-start]').dataset.offsetEnd,
+ }
+ })
+ }// }}}
+ render() {// {{{
+ const markedObject = this
this.marked = new Marked()
this.marked.use(markedTokenPosition())
this.marked.use({
@@ -265,7 +269,6 @@ export class MarkedPosition {
},
image(token) {
-
if (token.tokens) {
token.text = this.parser.parseInline(token.tokens, this.parser.textRenderer)
}
@@ -274,12 +277,11 @@ export class MarkedPosition {
return escapeHtmlEntities(token.text)
}
token.href = cleanHref
-
- let out = `
'
+ out += '>'
return out
},
@@ -291,8 +293,34 @@ export class MarkedPosition {
}
})
- }// }}}
+ }// }}}}}}
parse(text) {// {{{
return this.marked.parse(text)
}// }}}
+ async whenElementExist(id) {// {{{
+ // The element could have already been created.
+ const element = document.getElementById(id)
+ if (element) {
+ return element
+ }
+
+ const observer = new MutationObserver((_mutations, observer) => {
+ const target = document.getElementById(id)
+ if (target) {
+ observer.disconnect()
+ return target
+ }
+ })
+
+ observer.observe(document.documentElement, {
+ childList: true,
+ subtree: true
+ })
+ }// }}}
+ async populateImg(fileID, elementID) {// {{{
+ let img = await globalThis.nodeStore.files.get(fileID)
+ const el = await this.whenElementExist(elementID)
+
+ el.src = URL.createObjectURL(img.file)
+ }// }}}
}
diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs
index e849e29..d29923f 100644
--- a/static/js/node_store.mjs
+++ b/static/js/node_store.mjs
@@ -12,10 +12,11 @@ export class NodeStore {
this.nodes = {}
this.sendQueue = null
this.nodesHistory = null
+ this.files = null
}//}}}
initializeDB() {//{{{
return new Promise((resolve, reject) => {
- const req = indexedDB.open('notes', 7)
+ const req = indexedDB.open('notes', 8)
// Schema upgrades for IndexedDB.
// These can start from different points depending on updates to Notes2 since a device was online.
@@ -24,6 +25,7 @@ export class NodeStore {
let appState
let sendQueue
let nodesHistory
+ let files
const db = event.target.result
const trx = event.target.transaction
@@ -61,6 +63,10 @@ export class NodeStore {
case 7:
trx.objectStore('nodes_history').createIndex('byUUID', 'UUID', { unique: false })
break
+
+ case 8:
+ files = db.createObjectStore('files', { keyPath: 'UUID' })
+ break
}
}
}
@@ -69,6 +75,7 @@ export class NodeStore {
this.db = event.target.result
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history')
+ this.files = new SimpleNodeStore(this.db, 'files')
this.initializeRootNode()
.then(() => resolve())
}
@@ -159,39 +166,6 @@ export class NodeStore {
})
}//}}}
- /*
- upsertNodeRecords(records) {//{{{
- return new Promise((resolve, reject) => {
- const t = this.db.transaction('nodes', 'readwrite')
- const nodeStore = t.objectStore('nodes')
- t.onerror = (event) => {
- console.log('transaction error', event.target.error)
- reject(event.target.error)
- }
- t.oncomplete = () => {
- resolve()
- }
-
- // records is an object, not an array.
- for (const i in records) {
- const record = records[i]
-
- let addReq
- let op
- if (record.Deleted) {
- op = 'deleting'
- addReq = nodeStore.delete(record.UUID)
- } else {
- op = 'upserting'
- // 'modified' is a local property for tracking
- // nodes needing to be synced to backend.
- record.modified = 0
- addReq = nodeStore.put(record)
- }
- }
- })
- }//}}}
- */
getTreeNodes(parent, newLevel) {//{{{
return new Promise((resolve, reject) => {
// Parent of toplevel nodes is ROOT_NODE in indexedDB.
@@ -376,8 +350,20 @@ class SimpleNodeStore {
}
})
}//}}}
+ get(key) {//{{{
+ return new Promise((resolve, _reject) => {
+ const req = this.db
+ .transaction(['nodes', this.storeName], 'readonly')
+ .objectStore(this.storeName)
+ .get(key)
+
+ req.onsuccess = (event) => {
+ resolve(event.target.result)
+ }
+ })
+ }//}}}
retrieve(limit) {//{{{
- return new Promise((resolve, reject) => {
+ return new Promise((resolve, _reject) => {
const cursorReq = this.db
.transaction(['nodes', this.storeName], 'readonly')
.objectStore(this.storeName)
@@ -433,4 +419,51 @@ class SimpleNodeStore {
}//}}}
}
+export class StoreFile {
+ static createFromFileObject(f) {
+ const obj = new StoreFile()
+ obj.name = f.name
+ obj.size = f.size
+ obj.mime = f.type
+ return obj
+ }
+ constructor() {
+ this.name = ''
+ this.size = 0
+ this.mime = ''
+
+ this.objectURL = null // URL.createObjectURL(blob)
+ }
+ data() {
+ return {
+ }
+ }
+}
+
+export function uuidv7() {
+ // random bytes
+ const value = new Uint8Array(16)
+ crypto.getRandomValues(value)
+
+ // current timestamp in ms
+ const timestamp = BigInt(Date.now())
+
+ // timestamp
+ value[0] = Number((timestamp >> 40n) & 0xffn)
+ value[1] = Number((timestamp >> 32n) & 0xffn)
+ value[2] = Number((timestamp >> 24n) & 0xffn)
+ value[3] = Number((timestamp >> 16n) & 0xffn)
+ value[4] = Number((timestamp >> 8n) & 0xffn)
+ value[5] = Number(timestamp & 0xffn)
+
+ // version and variant
+ value[6] = (value[6] & 0x0f) | 0x70
+ value[8] = (value[8] & 0x3f) | 0x80
+
+ const str = Array.from(value)
+ .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/node.mjs b/static/js/page_node.mjs
similarity index 83%
rename from static/js/node.mjs
rename to static/js/page_node.mjs
index 7959831..86c29c0 100644
--- a/static/js/node.mjs
+++ b/static/js/page_node.mjs
@@ -1,8 +1,8 @@
-import { ROOT_NODE } from 'node_store'
+import { ROOT_NODE, uuidv7, StoreFile } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { MarkedPosition } from './marked_position.mjs'
-export class N2NodeUI extends CustomHTMLElement {
+export class N2PageNodeUI extends CustomHTMLElement {
static {// {{{
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
@@ -72,6 +72,7 @@ export class N2NodeUI extends CustomHTMLElement {
}
})
this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
+ this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event))
this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown()))
this.showMarkdown(true)
@@ -118,6 +119,47 @@ export class N2NodeUI extends CustomHTMLElement {
return this.classList.contains('show-markdown')
}
}// }}}
+ async pasteHandler(event) {
+ const clipboardItems = event.clipboardData?.items
+ if (!clipboardItems)
+ return
+
+ for (const item of clipboardItems) {
+ switch (item.kind) {
+ case 'string':
+ continue
+
+ case 'file':
+ const file = item.getAsFile()
+ if (!file)
+ throw new Error("Couldn't convert image to file object.")
+ const uuid = uuidv7()
+ await globalThis.nodeStore.files.add({ data: { UUID: uuid, file: file }})
+
+ const [start, end] = [this.elNodeContent.selectionStart, this.elNodeContent.selectionEnd]
+ this.elNodeContent.setRangeText(``, start, end, 'select');
+
+ break
+
+ default:
+ alert(`Unknown paste type of '${item.kind}'`)
+ }
+ }
+ }
+
+ // Example usage: Displaying the image or preparing it for upload
+ handleImageBlob(blob) {
+ // 1. Create a local URL to preview it in an
tag if needed
+ const localUrl = URL.createObjectURL(blob)
+ console.log('Local preview URL:', localUrl)
+
+ // 2. Or prepare it for a FormData upload
+ const formData = new FormData()
+ formData.append('image', blob, 'pasted-image.png')
+
+ // fetch('/upload', { method: 'POST', body: formData })
+ }
+
editMarkdown(data) {// {{{
this.showMarkdown(false)
this.elNodeContent.selectionStart = data.position.start
@@ -125,7 +167,7 @@ export class N2NodeUI extends CustomHTMLElement {
this.elNodeContent.focus()
}// }}}
}
-customElements.define('n2-nodeui', N2NodeUI)
+customElements.define('n2-nodeui', N2PageNodeUI)
export class Node {
static sort(a, b) {//{{{
@@ -260,30 +302,4 @@ export class Node {
}//}}}
}
-function uuidv7() {
- // random bytes
- const value = new Uint8Array(16)
- crypto.getRandomValues(value)
-
- // current timestamp in ms
- const timestamp = BigInt(Date.now())
-
- // timestamp
- value[0] = Number((timestamp >> 40n) & 0xffn)
- value[1] = Number((timestamp >> 32n) & 0xffn)
- value[2] = Number((timestamp >> 24n) & 0xffn)
- value[3] = Number((timestamp >> 16n) & 0xffn)
- value[4] = Number((timestamp >> 8n) & 0xffn)
- value[5] = Number(timestamp & 0xffn)
-
- // version and variant
- value[6] = (value[6] & 0x0f) | 0x70
- value[8] = (value[8] & 0x3f) | 0x80
-
- const str = Array.from(value)
- .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_storage.mjs b/static/js/page_storage.mjs
new file mode 100644
index 0000000..931a718
--- /dev/null
+++ b/static/js/page_storage.mjs
@@ -0,0 +1,28 @@
+import { CustomHTMLElement } from "./lib/custom_html_element.mjs"
+
+export class N2PageStorage extends CustomHTMLElement {
+ static {
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+ Local storage
+
+
+
+ `
+ }
+ constructor() {
+ super()
+
+ window._mbus.subscribe('SHOW_PAGE', () => this.render())
+ }
+ async render() {
+ const countNodes = await globalThis.nodeStore.nodeCount()
+ const countQueuedNodes = await globalThis.nodeStore.sendQueue.count()
+ const countHistoryNodes = await globalThis.nodeStore.nodesHistory.count()
+
+ this.elCountNodes.innerText = countNodes
+ this.elCountQueuedNodes.innerText = countQueuedNodes
+ this.elCountHistoryNodes.innerText = countHistoryNodes
+ }
+}
+customElements.define('n2-pagestorage', N2PageStorage)
diff --git a/static/js/sync.mjs b/static/js/sync.mjs
index 9b58cf7..e432f15 100644
--- a/static/js/sync.mjs
+++ b/static/js/sync.mjs
@@ -9,6 +9,9 @@ export class Sync {
}//}}}
async run() {//{{{
+ // XXX - Delete me
+ return
+
try {
let duration = 0 // in ms
@@ -163,13 +166,13 @@ export class Sync {
}
export class N2SyncProgress extends CustomHTMLElement {
- static {
+ static {// {{{
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
0 / 0
`
- }
+ }// }}}
constructor() {//{{{
super()
diff --git a/static/service_worker.js b/static/service_worker.js
index 1717eef..80b79a1 100644
--- a/static/service_worker.js
+++ b/static/service_worker.js
@@ -29,7 +29,8 @@ const CACHED_ASSETS = [
'/js/{{ .VERSION }}/lib/sjcl.js',
'/js/{{ .VERSION }}/marked_position.mjs',
'/js/{{ .VERSION }}/mbus.mjs',
- '/js/{{ .VERSION }}/node.mjs',
+ '/js/{{ .VERSION }}/page_node.mjs',
+ '/js/{{ .VERSION }}/page_storage.mjs',
'/js/{{ .VERSION }}/node_store.mjs',
'/js/{{ .VERSION }}/notes2.mjs',
'/js/{{ .VERSION }}/sync.mjs',
diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl
index c744166..ead95a9 100644
--- a/views/layouts/main.gotmpl
+++ b/views/layouts/main.gotmpl
@@ -14,7 +14,7 @@
"checklist": "/js/{{ .VERSION }}/checklist.mjs",
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
"node_store": "/js/{{ .VERSION }}/node_store.mjs",
- "node": "/js/{{ .VERSION }}/node.mjs",
+ "node": "/js/{{ .VERSION }}/page_node.mjs",
"tree": "/js/{{ .VERSION }}/tree.mjs"
}
}
diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl
index 77b74a6..80c084a 100644
--- a/views/pages/notes2.gotmpl
+++ b/views/pages/notes2.gotmpl
@@ -1,18 +1,34 @@
{{ define "page" }}