Upload files to IndexedDB
This commit is contained in:
parent
5bd5ef1f02
commit
8b421ea59e
15 changed files with 539 additions and 99 deletions
|
|
@ -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;
|
||||
|
|
|
|||
107
static/images/file_icons/application_pdf.svg
Normal file
107
static/images/file_icons/application_pdf.svg
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="31.218021"
|
||||
height="36"
|
||||
viewBox="0 0 8.2597682 9.5249997"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
sodipodi:docname="application_pdf.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="11.40625"
|
||||
inkscape:cy="20.9375"
|
||||
inkscape:window-width="2190"
|
||||
inkscape:window-height="1401"
|
||||
inkscape:window-x="1463"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-143.00122,-126.57004)"><title
|
||||
id="title1">file-outline</title><g
|
||||
id="g5"
|
||||
transform="matrix(0.59530539,0,0,0.59530539,142.96121,126.57024)"><g
|
||||
transform="matrix(0.04589,0,0,0.04589,-0.66877,-0.73379)"
|
||||
id="g1">
|
||||
<polygon
|
||||
points="282.65,102.07 282.65,356.65 51.791,356.65 51.791,23.99 204.5,23.99 "
|
||||
fill="#ffffff"
|
||||
stroke-width="212.65"
|
||||
id="polygon1" />
|
||||
<path
|
||||
d="m 201.19,31.99 73.46,73.393 v 243.26 H 59.79 V 31.983 h 141.4 m 6.623,-16 H 43.793 v 348.66 h 246.85 v -265.9 z"
|
||||
stroke-width="21.791"
|
||||
id="path1-8" />
|
||||
</g><g
|
||||
transform="matrix(0.04589,0,0,0.04589,-0.66877,-0.73379)"
|
||||
id="g2">
|
||||
<polygon
|
||||
points="51.791,23.99 204.5,23.99 206.31,25.8 206.31,100.33 280.9,100.33 282.65,102.07 282.65,356.65 51.791,356.65 "
|
||||
fill="#ffffff"
|
||||
stroke-width="212.65"
|
||||
id="polygon2" />
|
||||
<path
|
||||
d="m 198.31,31.99 v 76.337 h 76.337 v 240.32 H 59.787 V 31.987 h 138.52 m 9.5,-16 H 43.787 v 348.66 h 246.85 v -265.9 l -6.43,-6.424 H 214.3 V 22.481 Z"
|
||||
stroke-width="21.791"
|
||||
id="path2" />
|
||||
</g><g
|
||||
transform="matrix(0.04589,0,0,0.04589,-0.66877,-0.73379)"
|
||||
stroke-width="21.791"
|
||||
id="g3">
|
||||
<polygon
|
||||
points="219.64,48.667 258.31,86.38 258.31,87.75 219.64,87.75 "
|
||||
id="polygon3" />
|
||||
<path
|
||||
d="M 227.64,67.646 240.05,79.75 H 227.64 V 67.646 M 222.638,40.417 H 211.64 V 95.75 h 54.666 V 83.008 Z"
|
||||
id="path3" />
|
||||
</g><g
|
||||
transform="matrix(0.04589,0,0,0.04589,-0.66877,-0.73379)"
|
||||
fill="#ed1c24"
|
||||
stroke-width="212.65"
|
||||
id="g4">
|
||||
<polygon
|
||||
points="22.544,167.68 37.291,152.94 37.291,171.49 297.15,171.49 297.15,152.94 311.89,167.68 311.89,284.49 22.544,284.49 "
|
||||
id="polygon4" />
|
||||
<path
|
||||
d="m 303.65,168.63 1.747,1.747 v 107.62 H 29.047 v -107.62 l 1.747,-1.747 v 9.362 h 272.85 v -9.362 m -12.999,-31.385 v 27.747 H 43.785 v -27.747 l -27.747,27.747 v 126 h 302.35 v -126 z"
|
||||
id="path4" />
|
||||
</g><rect
|
||||
x="1.7219"
|
||||
y="7.9544001"
|
||||
width="10.684"
|
||||
height="4.0307002"
|
||||
fill="none"
|
||||
id="rect4" /><g
|
||||
transform="matrix(0.04589,0,0,0.04589,1.7219,11.733)"
|
||||
fill="#000000"
|
||||
stroke-width="21.791"
|
||||
aria-label="PDF"
|
||||
id="g7"><path
|
||||
d="M 9.216,0 V -83.2 H 39.68 q 6.784,0 12.928,1.408 6.144,1.28 10.752,4.608 4.608,3.2 7.296,8.576 2.816,5.248 2.816,13.056 0,7.68 -2.816,13.184 -2.688,5.504 -7.296,9.088 -4.608,3.456 -10.624,5.248 -6.016,1.664 -12.544,1.664 h -8.96 V 0 Z m 22.016,-43.776 h 7.936 q 6.528,0 9.6,-3.072 3.2,-3.072 3.2,-8.704 0,-5.632 -3.456,-7.936 -3.456,-2.304 -9.856,-2.304 h -7.424 z"
|
||||
id="path5"
|
||||
style="fill:#ffffff" /><path
|
||||
d="m 87.04,0 v -83.2 h 24.576 q 9.472,0 17.28,2.304 7.936,2.304 13.568,7.296 5.632,4.992 8.704,12.8 3.2,7.808 3.2,18.816 0,11.008 -3.072,18.944 -3.072,7.936 -8.704,13.056 -5.504,5.12 -13.184,7.552 Q 121.856,0 112.896,0 Z m 22.016,-17.664 h 1.28 q 4.48,0 8.448,-1.024 3.968,-1.152 6.784,-3.84 2.944,-2.688 4.608,-7.424 1.664,-4.736 1.664,-12.032 0,-7.296 -1.664,-11.904 -1.664,-4.608 -4.608,-7.168 -2.816,-2.56 -6.784,-3.456 -3.968,-1.024 -8.448,-1.024 h -1.28 z"
|
||||
id="path6"
|
||||
style="fill:#ffffff" /><path
|
||||
d="m 169.22,0 v -83.2 h 54.272 v 18.432 h -32.256 v 15.872 h 27.648 v 18.432 H 191.236 V 0 Z"
|
||||
id="path7"
|
||||
style="fill:#ffffff" /></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
100
static/images/file_icons/generic.svg
Normal file
100
static/images/file_icons/generic.svg
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="25.488195"
|
||||
height="36"
|
||||
viewBox="0 0 6.7437517 9.5249997"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
sodipodi:docname="application_pdf.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="8.53125"
|
||||
inkscape:cy="17.8125"
|
||||
inkscape:window-width="2190"
|
||||
inkscape:window-height="1401"
|
||||
inkscape:window-x="1463"
|
||||
inkscape:window-y="18"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-143.75929,-126.57004)"><title
|
||||
id="title1">file-outline</title><g
|
||||
id="g5"
|
||||
transform="matrix(0.59530539,0,0,0.59530539,142.96121,126.57024)"><g
|
||||
transform="matrix(0.04589,0,0,0.04589,-0.66877,-0.73379)"
|
||||
id="g1">
|
||||
<polygon
|
||||
points="51.791,356.65 51.791,23.99 204.5,23.99 282.65,102.07 282.65,356.65 "
|
||||
fill="#ffffff"
|
||||
stroke-width="212.65"
|
||||
id="polygon1" />
|
||||
<path
|
||||
d="m 201.19,31.99 73.46,73.393 v 243.26 H 59.79 V 31.983 h 141.4 m 6.623,-16 H 43.793 v 348.66 h 246.85 v -265.9 z"
|
||||
stroke-width="21.791"
|
||||
id="path1-8" />
|
||||
</g><g
|
||||
transform="matrix(0.04589,0,0,0.04589,-0.66877,-0.73379)"
|
||||
id="g2">
|
||||
<polygon
|
||||
points="206.31,25.8 206.31,100.33 280.9,100.33 282.65,102.07 282.65,356.65 51.791,356.65 51.791,23.99 204.5,23.99 "
|
||||
fill="#ffffff"
|
||||
stroke-width="212.65"
|
||||
id="polygon2" />
|
||||
<path
|
||||
d="m 198.31,31.99 v 76.337 h 76.337 v 240.32 H 59.787 V 31.987 h 138.52 m 9.5,-16 H 43.787 v 348.66 h 246.85 v -265.9 l -6.43,-6.424 H 214.3 V 22.481 Z"
|
||||
stroke-width="21.791"
|
||||
id="path2" />
|
||||
</g><g
|
||||
transform="matrix(0.04589,0,0,0.04589,-0.66877,-0.73379)"
|
||||
stroke-width="21.791"
|
||||
id="g3">
|
||||
<polygon
|
||||
points="258.31,87.75 219.64,87.75 219.64,48.667 258.31,86.38 "
|
||||
id="polygon3" />
|
||||
<path
|
||||
d="M 227.64,67.646 240.05,79.75 H 227.64 V 67.646 M 222.638,40.417 H 211.64 V 95.75 h 54.666 V 83.008 Z"
|
||||
id="path3" />
|
||||
</g><g
|
||||
transform="matrix(0.04589,0,0,0.04589,-0.66877,-0.73379)"
|
||||
fill="#ed1c24"
|
||||
stroke-width="212.65"
|
||||
id="g4">
|
||||
|
||||
|
||||
</g><rect
|
||||
x="1.7219"
|
||||
y="7.9544001"
|
||||
width="10.684"
|
||||
height="4.0307002"
|
||||
fill="none"
|
||||
id="rect4" /><g
|
||||
transform="matrix(0.04589,0,0,0.04589,1.7219,11.733)"
|
||||
fill="#000000"
|
||||
stroke-width="21.791"
|
||||
aria-label="PDF"
|
||||
id="g7"><path
|
||||
d="M 9.216,0 V -83.2 H 39.68 q 6.784,0 12.928,1.408 6.144,1.28 10.752,4.608 4.608,3.2 7.296,8.576 2.816,5.248 2.816,13.056 0,7.68 -2.816,13.184 -2.688,5.504 -7.296,9.088 -4.608,3.456 -10.624,5.248 -6.016,1.664 -12.544,1.664 h -8.96 V 0 Z m 22.016,-43.776 h 7.936 q 6.528,0 9.6,-3.072 3.2,-3.072 3.2,-8.704 0,-5.632 -3.456,-7.936 -3.456,-2.304 -9.856,-2.304 h -7.424 z"
|
||||
id="path5"
|
||||
style="fill:#ffffff" /><path
|
||||
d="m 87.04,0 v -83.2 h 24.576 q 9.472,0 17.28,2.304 7.936,2.304 13.568,7.296 5.632,4.992 8.704,12.8 3.2,7.808 3.2,18.816 0,11.008 -3.072,18.944 -3.072,7.936 -8.704,13.056 -5.504,5.12 -13.184,7.552 Q 121.856,0 112.896,0 Z m 22.016,-17.664 h 1.28 q 4.48,0 8.448,-1.024 3.968,-1.152 6.784,-3.84 2.944,-2.688 4.608,-7.424 1.664,-4.736 1.664,-12.032 0,-7.296 -1.664,-11.904 -1.664,-4.608 -4.608,-7.168 -2.816,-2.56 -6.784,-3.456 -3.968,-1.024 -8.448,-1.024 h -1.28 z"
|
||||
id="path6"
|
||||
style="fill:#ffffff" /></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
|
@ -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.
|
||||
|
|
|
|||
81
static/js/file.mjs
Normal file
81
static/js/file.mjs
Normal file
|
|
@ -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 = `
|
||||
<style>
|
||||
:host {
|
||||
display: inline-grid;
|
||||
grid-template-columns: min-content min-content;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
.el-image {
|
||||
max-width: var(--thumbnail-width);
|
||||
max-height: var(--thumbnail-height);
|
||||
}
|
||||
|
||||
.el-filename {
|
||||
display: none;
|
||||
font-weight: bold;
|
||||
background-color: #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<img data-el="image" src="/images/${_VERSION}/file_icons/generic.svg">
|
||||
<div data-el="filename"></div>
|
||||
`
|
||||
}
|
||||
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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = `<img ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" src="${token.href}" alt="${escapeHtmlEntities(token.text)}"`
|
||||
let out = `<n2-file ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" src="${token.href}" alt="${escapeHtmlEntities(token.text)}"`
|
||||
if (token.title) {
|
||||
out += ` title="${escapeHtmlEntities(token.title)}"`
|
||||
}
|
||||
out += '>'
|
||||
out += '></n2-file>'
|
||||
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)
|
||||
}// }}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <img> 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
|
||||
28
static/js/page_storage.mjs
Normal file
28
static/js/page_storage.mjs
Normal file
|
|
@ -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 = `
|
||||
<h1>Local storage</h1>
|
||||
<div data-el="count-nodes"></div>
|
||||
<div data-el="count-queued-nodes"></div>
|
||||
<div data-el="count-history-nodes"></div>
|
||||
`
|
||||
}
|
||||
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)
|
||||
|
|
@ -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 = `
|
||||
<progress data-el="progress" min=0 max=137 value=0></progress>
|
||||
<div data-el="count" class="count">0 / 0</div>
|
||||
`
|
||||
}
|
||||
}// }}}
|
||||
constructor() {//{{{
|
||||
super()
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue