Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b421ea59e | ||
|
|
5bd5ef1f02 | ||
|
|
43212a4487 | ||
|
|
6ce0cd838d |
|
|
@ -66,8 +66,6 @@ func (e *Engine) ReloadTemplates() { // {{{
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{
|
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.
|
// 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,
|
// To get rid of problems with cached content in browser on a new version release,
|
||||||
// while also not disabling cache altogether.
|
// 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])
|
r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2])
|
||||||
if e.DevMode {
|
if e.DevMode {
|
||||||
p := fmt.Sprintf("static/%s/%s", comp[1], comp[2])
|
e.staticLocalFS.ServeHTTP(w, r)
|
||||||
_, err = os.Stat(p)
|
|
||||||
if err == nil {
|
|
||||||
e.staticLocalFS.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,51 +52,51 @@ html {
|
||||||
#tree {
|
#tree {
|
||||||
grid-area: tree;
|
grid-area: tree;
|
||||||
display: grid;
|
display: grid;
|
||||||
background-color: #fafafa;
|
background-color: #ffffff;
|
||||||
color: #444;
|
color: #444;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
border-right: 1px solid #ddd;
|
border-right: 2px solid #ddd;
|
||||||
|
|
||||||
n2-tree {
|
|
||||||
/*border: 2px solid #f8f8f8;*/
|
|
||||||
padding: 16px 48px 16px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
n2-tree {
|
|
||||||
/*
|
|
||||||
border: 2px solid #fe5f55;
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#logo {
|
#logo {
|
||||||
display: grid;
|
display: grid;
|
||||||
position: relative;
|
grid-template-columns: min-content 1fr min-content;
|
||||||
justify-items: center;
|
align-items: center;
|
||||||
margin-top: 8px;
|
justify-items: start;
|
||||||
margin-bottom: 8px;
|
|
||||||
margin-left: 24px;
|
|
||||||
margin-right: 24px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
|
||||||
img {
|
.el-search {
|
||||||
width: 128px;
|
justify-self: end;
|
||||||
left: -20px;
|
}
|
||||||
|
|
||||||
|
img:first-child {
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icons {
|
.icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 32px;
|
margin: 16px 0px 32px 0px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n2-tree {
|
||||||
|
.el-treenodes {
|
||||||
|
margin: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
n2-tree {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.node {
|
.node {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 40px min-content;
|
grid-template-columns: 40px min-content;
|
||||||
|
|
@ -145,14 +145,27 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#tree-nodes {
|
[id^="page-"] {
|
||||||
padding: 16px 32px;
|
display: none;
|
||||||
/*
|
}
|
||||||
border-radius: 8px;
|
|
||||||
*/
|
#main-page {
|
||||||
/*
|
display: contents;
|
||||||
box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75);
|
|
||||||
*/
|
&.node {
|
||||||
|
#page-node {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.storage {
|
||||||
|
#page-storage {
|
||||||
|
display: contents;
|
||||||
|
n2-pagestorage {
|
||||||
|
grid-area: content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#crumbs {
|
#crumbs {
|
||||||
|
|
|
||||||
BIN
static/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -8,7 +8,7 @@
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
sodipodi:docname="collapsed.svg"
|
sodipodi:docname="collapsed.svg"
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -23,13 +23,13 @@
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:zoom="4.8373092"
|
inkscape:zoom="19.349237"
|
||||||
inkscape:cx="6.201795"
|
inkscape:cx="11.809251"
|
||||||
inkscape:cy="-12.40359"
|
inkscape:cy="6.3051583"
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="1093"
|
||||||
inkscape:window-height="1161"
|
inkscape:window-height="1401"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="2560"
|
||||||
inkscape:window-y="18"
|
inkscape:window-y="0"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
showguides="false" />
|
showguides="false" />
|
||||||
|
|
@ -42,9 +42,13 @@
|
||||||
transform="translate(-102.39375,-146.31458)">
|
transform="translate(-102.39375,-146.31458)">
|
||||||
<title
|
<title
|
||||||
id="title1">folder-outline</title>
|
id="title1">folder-outline</title>
|
||||||
|
<path
|
||||||
|
style="opacity:1;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
d="m 102.7356,147.34014 h 5.83884 v 3.91079 h -5.86619 z"
|
||||||
|
id="path2" />
|
||||||
<path
|
<path
|
||||||
d="m 108.34687,150.94479 h -5.29166 v -3.30729 h 5.29166 m 0,-0.66146 h -2.64584 l -0.66145,-0.66146 h -1.98437 c -0.36711,0 -0.66146,0.29435 -0.66146,0.66146 v 3.96875 a 0.66145729,0.66145729 0 0 0 0.66146,0.66146 h 5.29166 a 0.66145729,0.66145729 0 0 0 0.66146,-0.66146 v -3.30729 c 0,-0.36711 -0.29767,-0.66146 -0.66146,-0.66146 z"
|
d="m 108.34687,150.94479 h -5.29166 v -3.30729 h 5.29166 m 0,-0.66146 h -2.64584 l -0.66145,-0.66146 h -1.98437 c -0.36711,0 -0.66146,0.29435 -0.66146,0.66146 v 3.96875 a 0.66145729,0.66145729 0 0 0 0.66146,0.66146 h 5.29166 a 0.66145729,0.66145729 0 0 0 0.66146,-0.66146 v -3.30729 c 0,-0.36711 -0.29767,-0.66146 -0.66146,-0.66146 z"
|
||||||
id="path1"
|
id="path1"
|
||||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#71c837;fill-opacity:1;stroke-width:0.330728;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
|
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffcc00;fill-opacity:1;stroke-width:0.330728;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2.1 KiB |
|
|
@ -8,7 +8,7 @@
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
sodipodi:docname="expanded.svg"
|
sodipodi:docname="expanded.svg"
|
||||||
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
|
@ -23,13 +23,13 @@
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:zoom="11.17754"
|
inkscape:zoom="15.807429"
|
||||||
inkscape:cx="20.845374"
|
inkscape:cx="10.533022"
|
||||||
inkscape:cy="26.929003"
|
inkscape:cy="16.384701"
|
||||||
inkscape:window-width="1916"
|
inkscape:window-width="1093"
|
||||||
inkscape:window-height="1161"
|
inkscape:window-height="1401"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="1463"
|
||||||
inkscape:window-y="18"
|
inkscape:window-y="0"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1" /><defs
|
inkscape:current-layer="layer1" /><defs
|
||||||
id="defs1" /><g
|
id="defs1" /><g
|
||||||
|
|
@ -39,6 +39,9 @@
|
||||||
transform="translate(-101.33542,-147.10833)"><title
|
transform="translate(-101.33542,-147.10833)"><title
|
||||||
id="title1">folder-open</title><title
|
id="title1">folder-open</title><title
|
||||||
id="title1-1">folder-open-outline</title><path
|
id="title1-1">folder-open-outline</title><path
|
||||||
|
style="opacity:1;fill:#ffffff;stroke-width:0.264583;fill-opacity:1"
|
||||||
|
d="m 101.61996,148.02892 5.99218,0.36823 v 1.12144 l 0.16738,0.33476 -0.703,2.32657 -5.22222,-0.11717 z"
|
||||||
|
id="path2" /><path
|
||||||
d="m 102.69141,149.0927 -0.69454,2.64584 v -3.30729 h 5.62239 a 0.6614573,0.6614573 0 0 0 -0.66146,-0.66146 h -2.3151 l -0.66146,-0.66146 h -1.98437 a 0.6614573,0.6614573 0 0 0 -0.66145,0.66146 v 3.96875 a 0.6614573,0.6614573 0 0 0 0.66145,0.66146 h 4.96093 c 0.29766,0 0.56224,-0.19844 0.62839,-0.4961 l 0.76067,-2.8112 h -5.65545 m 4.26639,2.64584 h -4.29947 l 0.52916,-1.98438 h 4.29948 z"
|
d="m 102.69141,149.0927 -0.69454,2.64584 v -3.30729 h 5.62239 a 0.6614573,0.6614573 0 0 0 -0.66146,-0.66146 h -2.3151 l -0.66146,-0.66146 h -1.98437 a 0.6614573,0.6614573 0 0 0 -0.66145,0.66146 v 3.96875 a 0.6614573,0.6614573 0 0 0 0.66145,0.66146 h 4.96093 c 0.29766,0 0.56224,-0.19844 0.62839,-0.4961 l 0.76067,-2.8112 h -5.65545 m 4.26639,2.64584 h -4.29947 l 0.52916,-1.98438 h 4.29948 z"
|
||||||
id="path1"
|
id="path1"
|
||||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#71c837;fill-opacity:1;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" /></g></svg>
|
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffcc00;fill-opacity:1;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" /></g></svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.3 KiB |
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
|
|
@ -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 |
49
static/images/icon_settings.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="23.350698"
|
||||||
|
height="23.999699"
|
||||||
|
viewBox="0 0 6.1782055 6.3499202"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
sodipodi:docname="icon_settings.svg"
|
||||||
|
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="1"
|
||||||
|
inkscape:cx="-144.5"
|
||||||
|
inkscape:cy="-132"
|
||||||
|
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(-146.12156,-110.33135)">
|
||||||
|
<title
|
||||||
|
id="title1">cog-outline</title>
|
||||||
|
<path
|
||||||
|
d="m 149.21069,112.23625 a 1.2700039,1.2700039 0 0 1 1.27001,1.27 1.2700039,1.2700039 0 0 1 -1.27001,1.27001 1.2700039,1.2700039 0 0 1 -1.27,-1.27001 1.2700039,1.2700039 0 0 1 1.27,-1.27 m 0,0.63501 a 0.63500199,0.63500199 0 0 0 -0.635,0.63499 0.63500199,0.63500199 0 0 0 0.635,0.63501 0.63500199,0.63500199 0 0 0 0.635,-0.63501 0.63500199,0.63500199 0 0 0 -0.635,-0.63499 m -0.635,3.81001 c -0.0794,0 -0.14605,-0.0571 -0.15875,-0.13336 l -0.11748,-0.84137 c -0.20002,-0.0793 -0.37147,-0.18733 -0.53658,-0.31433 l -0.79057,0.32068 c -0.0698,0.0254 -0.15557,0 -0.19367,-0.0698 l -0.63501,-1.09855 c -0.0413,-0.0699 -0.0222,-0.15557 0.038,-0.2032 l 0.66993,-0.52706 -0.0222,-0.30798 0.0222,-0.31749 -0.66993,-0.51753 c -0.0604,-0.0476 -0.0793,-0.13336 -0.038,-0.2032 l 0.63501,-1.09855 c 0.0382,-0.0698 0.12382,-0.0984 0.19367,-0.0698 l 0.79057,0.3175 c 0.16511,-0.12382 0.33656,-0.23177 0.53658,-0.31115 l 0.11748,-0.84137 c 0.0127,-0.0762 0.0793,-0.13336 0.15875,-0.13336 h 1.27 c 0.0793,0 0.14606,0.0571 0.15875,0.13336 l 0.11748,0.84137 c 0.20003,0.0794 0.37148,0.18733 0.53657,0.31115 l 0.79059,-0.3175 c 0.0698,-0.0286 0.15557,0 0.19367,0.0698 l 0.635,1.09855 c 0.0413,0.0698 0.0222,0.15557 -0.0382,0.2032 l -0.66992,0.51753 0.0222,0.31749 -0.0222,0.31751 0.66992,0.51753 c 0.0604,0.0476 0.0793,0.13334 0.0382,0.2032 l -0.635,1.09855 c -0.0382,0.0698 -0.12383,0.0984 -0.19367,0.0698 l -0.79059,-0.31751 c -0.16509,0.12383 -0.33654,0.23178 -0.53657,0.31116 l -0.11748,0.84137 c -0.0127,0.0762 -0.0793,0.13336 -0.15875,0.13336 h -1.27 m 0.39688,-5.71502 -0.11748,0.82868 c -0.381,0.0794 -0.71755,0.28257 -0.96203,0.56515 l -0.76517,-0.3302 -0.23813,0.41275 0.66993,0.49212 c -0.127,0.37149 -0.127,0.77471 0,1.143 l -0.67311,0.49531 0.23813,0.41275 0.77153,-0.33019 c 0.24448,0.27939 0.57785,0.48259 0.95567,0.55879 l 0.11748,0.83185 h 0.48261 l 0.11748,-0.82867 c 0.37783,-0.0793 0.7112,-0.28258 0.95568,-0.56197 l 0.77153,0.33019 0.23812,-0.41275 -0.6731,-0.49213 c 0.127,-0.37147 0.127,-0.77469 0,-1.14618 l 0.66993,-0.49212 -0.23813,-0.41275 -0.76518,0.3302 c -0.24447,-0.28258 -0.58102,-0.48577 -0.96202,-0.56197 l -0.11748,-0.83186 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:0.317501;fill:#000000;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6 KiB |
63
static/images/logo_small.svg
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="28.593159"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 7.5652731 5.2916666"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
sodipodi:docname="logo_small.svg"
|
||||||
|
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.477217"
|
||||||
|
inkscape:cx="48.460855"
|
||||||
|
inkscape:cy="5.2193281"
|
||||||
|
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(-126.73541,-178.59375)">
|
||||||
|
<rect
|
||||||
|
style="fill:#a02c2c;stroke:none;stroke-width:0.113818"
|
||||||
|
id="rect5"
|
||||||
|
width="7.5652733"
|
||||||
|
height="5.2916665"
|
||||||
|
x="126.73541"
|
||||||
|
y="178.59375"
|
||||||
|
ry="1.0060203" />
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.82235px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#ffffff;stroke:none;stroke-width:0.40186"
|
||||||
|
x="112.25369"
|
||||||
|
y="207.99469"
|
||||||
|
id="text5"
|
||||||
|
transform="scale(1.1392149,0.87779751)"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan5"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.82235px;font-family:'Forgotten Futurist';-inkscape-font-specification:'Forgotten Futurist, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;stroke:none;stroke-width:0.40186"
|
||||||
|
x="112.25369"
|
||||||
|
y="207.99469">N2</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -29,6 +29,14 @@ export class App {
|
||||||
this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
|
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('keydown', event => this.keyHandler(event))
|
||||||
window.addEventListener('popstate', event => this.popState(event))
|
window.addEventListener('popstate', event => this.popState(event))
|
||||||
document.getElementById('notes2').addEventListener('click', event => {
|
document.getElementById('notes2').addEventListener('click', event => {
|
||||||
|
|
@ -36,6 +44,8 @@ export class App {
|
||||||
document.getElementById('node-content')?.focus()
|
document.getElementById('node-content')?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
|
||||||
|
|
||||||
window._sync = new Sync()
|
window._sync = new Sync()
|
||||||
|
|
||||||
// I think it is uncomfortable having the sync running as soon as the page load.
|
// I think it is uncomfortable having the sync running as soon as the page load.
|
||||||
|
|
|
||||||
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)
|
||||||
207
static/js/lib/css_colorize.mjs
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
export class Color {
|
||||||
|
constructor(r, g, b) { this.set(r, g, b); }
|
||||||
|
toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }
|
||||||
|
|
||||||
|
set(r, g, b) {
|
||||||
|
this.r = this.clamp(r);
|
||||||
|
this.g = this.clamp(g);
|
||||||
|
this.b = this.clamp(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
hueRotate(angle = 0) {
|
||||||
|
angle = angle / 180 * Math.PI;
|
||||||
|
let sin = Math.sin(angle);
|
||||||
|
let cos = Math.cos(angle);
|
||||||
|
|
||||||
|
this.multiply([
|
||||||
|
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
|
||||||
|
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
|
||||||
|
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
grayscale(value = 1) {
|
||||||
|
this.multiply([
|
||||||
|
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
|
||||||
|
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
|
||||||
|
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sepia(value = 1) {
|
||||||
|
this.multiply([
|
||||||
|
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
|
||||||
|
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
|
||||||
|
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
saturate(value = 1) {
|
||||||
|
this.multiply([
|
||||||
|
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
|
||||||
|
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
|
||||||
|
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
multiply(matrix) {
|
||||||
|
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
|
||||||
|
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
|
||||||
|
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
|
||||||
|
this.r = newR; this.g = newG; this.b = newB;
|
||||||
|
}
|
||||||
|
|
||||||
|
brightness(value = 1) { this.linear(value); }
|
||||||
|
contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
|
||||||
|
|
||||||
|
linear(slope = 1, intercept = 0) {
|
||||||
|
this.r = this.clamp(this.r * slope + intercept * 255);
|
||||||
|
this.g = this.clamp(this.g * slope + intercept * 255);
|
||||||
|
this.b = this.clamp(this.b * slope + intercept * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
invert(value = 1) {
|
||||||
|
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
|
||||||
|
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
|
||||||
|
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
|
||||||
|
let r = this.r / 255;
|
||||||
|
let g = this.g / 255;
|
||||||
|
let b = this.b / 255;
|
||||||
|
let max = Math.max(r, g, b);
|
||||||
|
let min = Math.min(r, g, b);
|
||||||
|
let h, s, l = (max + min) / 2;
|
||||||
|
|
||||||
|
if (max === min) {
|
||||||
|
h = s = 0;
|
||||||
|
} else {
|
||||||
|
let d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
switch (max) {
|
||||||
|
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||||
|
case g: h = (b - r) / d + 2; break;
|
||||||
|
case b: h = (r - g) / d + 4; break;
|
||||||
|
} h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
h: h * 100,
|
||||||
|
s: s * 100,
|
||||||
|
l: l * 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clamp(value) {
|
||||||
|
if (value > 255) { value = 255; }
|
||||||
|
else if (value < 0) { value = 0; }
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Solver {
|
||||||
|
constructor(target) {
|
||||||
|
this.target = target;
|
||||||
|
this.targetHSL = target.hsl();
|
||||||
|
this.reusedColor = new Color(0, 0, 0); // Object pool
|
||||||
|
}
|
||||||
|
|
||||||
|
solve() {
|
||||||
|
let result = this.solveNarrow(this.solveWide());
|
||||||
|
return {
|
||||||
|
values: result.values,
|
||||||
|
loss: result.loss,
|
||||||
|
filter: this.css(result.values)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
solveWide() {
|
||||||
|
const A = 5;
|
||||||
|
const c = 15;
|
||||||
|
const a = [60, 180, 18000, 600, 1.2, 1.2];
|
||||||
|
|
||||||
|
let best = { loss: Infinity };
|
||||||
|
for (let i = 0; best.loss > 25 && i < 3; i++) {
|
||||||
|
let initial = [50, 20, 3750, 50, 100, 100];
|
||||||
|
let result = this.spsa(A, a, c, initial, 1000);
|
||||||
|
if (result.loss < best.loss) { best = result; }
|
||||||
|
} return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
solveNarrow(wide) {
|
||||||
|
const A = wide.loss;
|
||||||
|
const c = 2;
|
||||||
|
const A1 = A + 1;
|
||||||
|
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
|
||||||
|
return this.spsa(A, a, c, wide.values, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
spsa(A, a, c, values, iters) {
|
||||||
|
const alpha = 1;
|
||||||
|
const gamma = 0.16666666666666666;
|
||||||
|
|
||||||
|
let best = null;
|
||||||
|
let bestLoss = Infinity;
|
||||||
|
let deltas = new Array(6);
|
||||||
|
let highArgs = new Array(6);
|
||||||
|
let lowArgs = new Array(6);
|
||||||
|
|
||||||
|
for (let k = 0; k < iters; k++) {
|
||||||
|
let ck = c / Math.pow(k + 1, gamma);
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
deltas[i] = Math.random() > 0.5 ? 1 : -1;
|
||||||
|
highArgs[i] = values[i] + ck * deltas[i];
|
||||||
|
lowArgs[i] = values[i] - ck * deltas[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
let g = lossDiff / (2 * ck) * deltas[i];
|
||||||
|
let ak = a[i] / Math.pow(A + k + 1, alpha);
|
||||||
|
values[i] = fix(values[i] - ak * g, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let loss = this.loss(values);
|
||||||
|
if (loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
|
||||||
|
} return { values: best, loss: bestLoss };
|
||||||
|
|
||||||
|
function fix(value, idx) {
|
||||||
|
let max = 100;
|
||||||
|
if (idx === 2 /* saturate */) { max = 7500; }
|
||||||
|
else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }
|
||||||
|
|
||||||
|
if (idx === 3 /* hue-rotate */) {
|
||||||
|
if (value > max) { value = value % max; }
|
||||||
|
else if (value < 0) { value = max + value % max; }
|
||||||
|
} else if (value < 0) { value = 0; }
|
||||||
|
else if (value > max) { value = max; }
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loss(filters) { // Argument is array of percentages.
|
||||||
|
let color = this.reusedColor;
|
||||||
|
color.set(0, 0, 0);
|
||||||
|
|
||||||
|
color.invert(filters[0] / 100);
|
||||||
|
color.sepia(filters[1] / 100);
|
||||||
|
color.saturate(filters[2] / 100);
|
||||||
|
color.hueRotate(filters[3] * 3.6);
|
||||||
|
color.brightness(filters[4] / 100);
|
||||||
|
color.contrast(filters[5] / 100);
|
||||||
|
|
||||||
|
let colorHSL = color.hsl();
|
||||||
|
return Math.abs(color.r - this.target.r)
|
||||||
|
+ Math.abs(color.g - this.target.g)
|
||||||
|
+ Math.abs(color.b - this.target.b)
|
||||||
|
+ Math.abs(colorHSL.h - this.targetHSL.h)
|
||||||
|
+ Math.abs(colorHSL.s - this.targetHSL.s)
|
||||||
|
+ Math.abs(colorHSL.l - this.targetHSL.l);
|
||||||
|
}
|
||||||
|
|
||||||
|
css(filters) {
|
||||||
|
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
|
||||||
|
return `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
export class CustomHTMLElement extends HTMLElement {
|
export class CustomHTMLElement extends HTMLElement {
|
||||||
constructor() {// {{{
|
constructor(useShadow) {// {{{
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.appendChild(this.constructor.tmpl.content.cloneNode(true))
|
const workOn = useShadow ? this.attachShadow({ mode: 'open' }) : this
|
||||||
|
workOn.appendChild(this.constructor.tmpl.content.cloneNode(true))
|
||||||
this.querySelectorAll('*').forEach(el => {
|
workOn.querySelectorAll('*').forEach(el => {
|
||||||
const field = el.dataset.field
|
const field = el.dataset.field
|
||||||
if (field !== undefined) {
|
if (field !== undefined) {
|
||||||
const fieldName = this.toElementName('field', field)
|
const fieldName = this.toElementName('field', field)
|
||||||
|
|
|
||||||
|
|
@ -92,30 +92,34 @@ function escapeHtmlEntities(html, encode) {// {{{
|
||||||
|
|
||||||
export class MarkedPosition {
|
export class MarkedPosition {
|
||||||
constructor() {// {{{
|
constructor() {// {{{
|
||||||
window.setpos = (event) => {
|
window.setpos = (event) => this.setpos(event)
|
||||||
event.stopPropagation()
|
this.render()
|
||||||
event.preventDefault()
|
}// }}}
|
||||||
|
setpos(event) {// {{{
|
||||||
_mbus.dispatch('MARKDOWN_EDIT', {
|
event.stopPropagation()
|
||||||
position: {
|
event.preventDefault()
|
||||||
start: event.target.dataset.offsetStart,
|
|
||||||
end: event.target.dataset.offsetEnd,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
_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 = new Marked()
|
||||||
this.marked.use(markedTokenPosition())
|
this.marked.use(markedTokenPosition())
|
||||||
this.marked.use({
|
this.marked.use({
|
||||||
renderer: {
|
renderer: {
|
||||||
heading(token) {
|
heading(token) {
|
||||||
const content = this.parser.parseInline(token.tokens)
|
const content = this.parser.parseInline(token.tokens)
|
||||||
return `<h${token.depth} onclick="setpos(event)" onclick="setpos(this)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</h${token.depth}>\n`
|
return `<h${token.depth} ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</h${token.depth}>\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
paragraph(token) {
|
paragraph(token) {
|
||||||
const content = this.parser.parseInline(token.tokens)
|
const content = this.parser.parseInline(token.tokens)
|
||||||
return `<p onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</p>\n`
|
return `<p ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</p>\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
list(token) {
|
list(token) {
|
||||||
|
|
@ -134,7 +138,7 @@ export class MarkedPosition {
|
||||||
},
|
},
|
||||||
|
|
||||||
listitem(token) {
|
listitem(token) {
|
||||||
return `<li onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parse(token.tokens)}</li>\n`
|
return `<li ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parse(token.tokens)}</li>\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
code(token) {
|
code(token) {
|
||||||
|
|
@ -143,12 +147,12 @@ export class MarkedPosition {
|
||||||
const code = token.text.replace(other.endingNewline, '') + '\n'
|
const code = token.text.replace(other.endingNewline, '') + '\n'
|
||||||
|
|
||||||
if (!langString) {
|
if (!langString) {
|
||||||
return `<pre onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code>`
|
return `<pre ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code>`
|
||||||
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
||||||
+ '</code></pre>\n'
|
+ '</code></pre>\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<pre onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code class="language-`
|
return `<pre ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code class="language-`
|
||||||
+ escapeHtmlEntities(langString)
|
+ escapeHtmlEntities(langString)
|
||||||
+ '">'
|
+ '">'
|
||||||
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
||||||
|
|
@ -157,7 +161,7 @@ export class MarkedPosition {
|
||||||
|
|
||||||
blockquote(token) {
|
blockquote(token) {
|
||||||
const body = this.parser.parse(token.tokens)
|
const body = this.parser.parse(token.tokens)
|
||||||
return `<blockquote onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n${body}</blockquote>\n`
|
return `<blockquote ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n${body}</blockquote>\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
html(token) {
|
html(token) {
|
||||||
|
|
@ -169,11 +173,11 @@ export class MarkedPosition {
|
||||||
},
|
},
|
||||||
|
|
||||||
hr(token) {
|
hr(token) {
|
||||||
return `<hr onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n`
|
return `<hr ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
checkbox(token) {
|
checkbox(token) {
|
||||||
return `<input onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"`
|
return `<input ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"`
|
||||||
+ (token.checked ? 'checked="" ' : '')
|
+ (token.checked ? 'checked="" ' : '')
|
||||||
+ 'disabled="" type="checkbox"> '
|
+ 'disabled="" type="checkbox"> '
|
||||||
},
|
},
|
||||||
|
|
@ -218,7 +222,7 @@ export class MarkedPosition {
|
||||||
if (token.tokens.length > 0) {
|
if (token.tokens.length > 0) {
|
||||||
const start = token.tokens[0].position.start.offset
|
const start = token.tokens[0].position.start.offset
|
||||||
const end = token.tokens[0].position.end.offset
|
const end = token.tokens[0].position.end.offset
|
||||||
ofs = `onclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
|
ofs = `ondblclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = this.parser.parseInline(token.tokens);
|
const content = this.parser.parseInline(token.tokens);
|
||||||
|
|
@ -230,23 +234,23 @@ export class MarkedPosition {
|
||||||
},
|
},
|
||||||
|
|
||||||
strong(token) {
|
strong(token) {
|
||||||
return `<strong onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</strong>`
|
return `<strong ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</strong>`
|
||||||
},
|
},
|
||||||
|
|
||||||
em(token) {
|
em(token) {
|
||||||
return `<em onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</em>`
|
return `<em ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</em>`
|
||||||
},
|
},
|
||||||
|
|
||||||
codespan(token) {
|
codespan(token) {
|
||||||
return `<code onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${escapeHtmlEntities(token.text, true)}</code>`
|
return `<code ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${escapeHtmlEntities(token.text, true)}</code>`
|
||||||
},
|
},
|
||||||
|
|
||||||
br(token) {
|
br(token) {
|
||||||
return `<br onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">`
|
return `<br ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">`
|
||||||
},
|
},
|
||||||
|
|
||||||
del(token) {
|
del(token) {
|
||||||
return `<del onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</del>`
|
return `<del ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</del>`
|
||||||
},
|
},
|
||||||
|
|
||||||
link(token) {
|
link(token) {
|
||||||
|
|
@ -256,7 +260,7 @@ export class MarkedPosition {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
token.href = cleanHref
|
token.href = cleanHref
|
||||||
let out = '<a onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" href="' + token.href + '"'
|
let out = '<a ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" href="' + token.href + '"'
|
||||||
if (token.title) {
|
if (token.title) {
|
||||||
out += ' title="' + (escapeHtmlEntities(token.title)) + '"'
|
out += ' title="' + (escapeHtmlEntities(token.title)) + '"'
|
||||||
}
|
}
|
||||||
|
|
@ -265,7 +269,6 @@ export class MarkedPosition {
|
||||||
},
|
},
|
||||||
|
|
||||||
image(token) {
|
image(token) {
|
||||||
|
|
||||||
if (token.tokens) {
|
if (token.tokens) {
|
||||||
token.text = this.parser.parseInline(token.tokens, this.parser.textRenderer)
|
token.text = this.parser.parseInline(token.tokens, this.parser.textRenderer)
|
||||||
}
|
}
|
||||||
|
|
@ -274,12 +277,11 @@ export class MarkedPosition {
|
||||||
return escapeHtmlEntities(token.text)
|
return escapeHtmlEntities(token.text)
|
||||||
}
|
}
|
||||||
token.href = cleanHref
|
token.href = cleanHref
|
||||||
|
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)}"`
|
||||||
let out = `<img onclick="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) {
|
if (token.title) {
|
||||||
out += ` title="${escapeHtmlEntities(token.title)}"`
|
out += ` title="${escapeHtmlEntities(token.title)}"`
|
||||||
}
|
}
|
||||||
out += '>'
|
out += '></n2-file>'
|
||||||
return out
|
return out
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -291,8 +293,34 @@ export class MarkedPosition {
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}}}}
|
||||||
parse(text) {// {{{
|
parse(text) {// {{{
|
||||||
return this.marked.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.nodes = {}
|
||||||
this.sendQueue = null
|
this.sendQueue = null
|
||||||
this.nodesHistory = null
|
this.nodesHistory = null
|
||||||
|
this.files = null
|
||||||
}//}}}
|
}//}}}
|
||||||
initializeDB() {//{{{
|
initializeDB() {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const req = indexedDB.open('notes', 7)
|
const req = indexedDB.open('notes', 8)
|
||||||
|
|
||||||
// Schema upgrades for IndexedDB.
|
// Schema upgrades for IndexedDB.
|
||||||
// These can start from different points depending on updates to Notes2 since a device was online.
|
// 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 appState
|
||||||
let sendQueue
|
let sendQueue
|
||||||
let nodesHistory
|
let nodesHistory
|
||||||
|
let files
|
||||||
const db = event.target.result
|
const db = event.target.result
|
||||||
const trx = event.target.transaction
|
const trx = event.target.transaction
|
||||||
|
|
||||||
|
|
@ -61,6 +63,10 @@ export class NodeStore {
|
||||||
case 7:
|
case 7:
|
||||||
trx.objectStore('nodes_history').createIndex('byUUID', 'UUID', { unique: false })
|
trx.objectStore('nodes_history').createIndex('byUUID', 'UUID', { unique: false })
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 8:
|
||||||
|
files = db.createObjectStore('files', { keyPath: 'UUID' })
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +75,7 @@ export class NodeStore {
|
||||||
this.db = event.target.result
|
this.db = event.target.result
|
||||||
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
|
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
|
||||||
this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history')
|
this.nodesHistory = new SimpleNodeStore(this.db, 'nodes_history')
|
||||||
|
this.files = new SimpleNodeStore(this.db, 'files')
|
||||||
this.initializeRootNode()
|
this.initializeRootNode()
|
||||||
.then(() => resolve())
|
.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) {//{{{
|
getTreeNodes(parent, newLevel) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Parent of toplevel nodes is ROOT_NODE in indexedDB.
|
// 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) {//{{{
|
retrieve(limit) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
const cursorReq = this.db
|
const cursorReq = this.db
|
||||||
.transaction(['nodes', this.storeName], 'readonly')
|
.transaction(['nodes', this.storeName], 'readonly')
|
||||||
.objectStore(this.storeName)
|
.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
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -86,404 +86,6 @@ export class Notes2 extends Component {
|
||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Tree extends Component {
|
|
||||||
constructor(props) {//{{{
|
|
||||||
super(props)
|
|
||||||
console.log('new tree')
|
|
||||||
this.treeNodeComponents = {}
|
|
||||||
this.treeTrunk = []
|
|
||||||
this.selectedNode = null
|
|
||||||
this.expandedNodes = {} // keyed on UUID
|
|
||||||
this.treeDiv = createRef()
|
|
||||||
|
|
||||||
// childrenFetchedCallbacks is keyed on a UUID and each
|
|
||||||
// item is an array with callbacks called when a UUID has
|
|
||||||
// had all children fetched.
|
|
||||||
this.childrenFetchedCallbacks = {}
|
|
||||||
|
|
||||||
this.props.app.tree = this
|
|
||||||
|
|
||||||
this.populateFirstLevel()
|
|
||||||
}//}}}
|
|
||||||
render({ app }) {//{{{
|
|
||||||
const renderedTreeTrunk = this.treeTrunk.map(node => {
|
|
||||||
this.treeNodeComponents[node.UUID] = createRef()
|
|
||||||
return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />`
|
|
||||||
})
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div id="tree" ref=${this.treeDiv} tabindex="0">
|
|
||||||
<div id="logo" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}><img src="/images/${_VERSION}/logo.svg" /></div>
|
|
||||||
<div class="icons">
|
|
||||||
<img src="/images/${_VERSION}/icon_search.svg" style="height: 22px" onclick=${() => _mbus.dispatch('op-search')} />
|
|
||||||
<img src="/images/${_VERSION}/icon_refresh.svg" onclick=${() => _sync.run()} />
|
|
||||||
</div>
|
|
||||||
${renderedTreeTrunk}
|
|
||||||
</div>`
|
|
||||||
}//}}}
|
|
||||||
componentDidMount() {//{{{
|
|
||||||
//this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event))
|
|
||||||
|
|
||||||
// This will show and select the treenode that is selected in the node UI.
|
|
||||||
const node = _notes2.current?.nodeUI.current?.node.value
|
|
||||||
if (node === null)
|
|
||||||
return
|
|
||||||
_notes2.current.tree.expandToTrunk(node)
|
|
||||||
this.setSelected(node)
|
|
||||||
}//}}}
|
|
||||||
|
|
||||||
fetchChildrenNotify(uuid, fn) {//{{{
|
|
||||||
if (this.childrenFetchedCallbacks[uuid] === undefined)
|
|
||||||
this.childrenFetchedCallbacks[uuid] = [fn]
|
|
||||||
else
|
|
||||||
this.childrenFetchedCallbacks[uuid].push(fn)
|
|
||||||
}//}}}
|
|
||||||
fetchChildrenOn(uuid) {//{{{
|
|
||||||
if (this.childrenFetchedCallbacks[uuid] === undefined)
|
|
||||||
return
|
|
||||||
for (const fn of this.childrenFetchedCallbacks[uuid])
|
|
||||||
fn(uuid)
|
|
||||||
delete this.childrenFetchedCallbacks[uuid]
|
|
||||||
}//}}}
|
|
||||||
|
|
||||||
populateFirstLevel(callback = null) {//{{{
|
|
||||||
nodeStore.get(ROOT_NODE)
|
|
||||||
.then(node => node.fetchChildren())
|
|
||||||
.then(children => {
|
|
||||||
this.treeNodeComponents = {}
|
|
||||||
this.treeTrunk = []
|
|
||||||
for (const node of children) {
|
|
||||||
// The root node isn't supposed to be shown in the tree.
|
|
||||||
if (node.UUID === ROOT_NODE)
|
|
||||||
continue
|
|
||||||
if (node.ParentUUID === ROOT_NODE)
|
|
||||||
this.treeTrunk.push(node)
|
|
||||||
}
|
|
||||||
this.forceUpdate()
|
|
||||||
if (callback)
|
|
||||||
callback()
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) })
|
|
||||||
}//}}}
|
|
||||||
setSelected(node, dontExpand) {//{{{
|
|
||||||
// The previously selected node, if any, needs to be rerendered
|
|
||||||
// to not retain its 'selected' class.
|
|
||||||
const prevUUID = this.selectedNode?.UUID
|
|
||||||
this.selectedNode = node
|
|
||||||
if (prevUUID)
|
|
||||||
this.treeNodeComponents[prevUUID]?.current.forceUpdate()
|
|
||||||
|
|
||||||
// And now the newly selected node is rerendered.
|
|
||||||
this.treeNodeComponents[node.UUID]?.current.forceUpdate()
|
|
||||||
|
|
||||||
if (!dontExpand)
|
|
||||||
this.setNodeExpanded(node, true)
|
|
||||||
}//}}}
|
|
||||||
isSelected(node) {//{{{
|
|
||||||
return this.selectedNode?.UUID === node.UUID
|
|
||||||
}//}}}
|
|
||||||
async expandToTrunk(node) {//{{{
|
|
||||||
// Get all ancestors from a certain node up to the highest grandparent.
|
|
||||||
const ancestry = await nodeStore.getNodeAncestry(node, [])
|
|
||||||
for (const i in ancestry) {
|
|
||||||
await nodeStore.node(ancestry[i].UUID).fetchChildren()
|
|
||||||
this.setNodeExpanded(ancestry[i], true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Already a top node, no need to expand anything.
|
|
||||||
if (ancestry.length === 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Start the chain of by expanding the top node.
|
|
||||||
this.setNodeExpanded(ancestry[ancestry.length - 1], true)
|
|
||||||
}//}}}
|
|
||||||
getNodeExpanded(UUID) {//{{{
|
|
||||||
if (this.expandedNodes[UUID] === undefined)
|
|
||||||
this.expandedNodes[UUID] = signal(false)
|
|
||||||
return this.expandedNodes[UUID].value
|
|
||||||
}//}}}
|
|
||||||
async setNodeExpanded(node, value) {//{{{
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const work = uuid => {
|
|
||||||
// Creating a default value if it doesn't exist already.
|
|
||||||
this.getNodeExpanded(uuid)
|
|
||||||
this.expandedNodes[uuid].value = value
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.hasFetchedChildren()) {
|
|
||||||
work(node.UUID)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
this.fetchChildrenNotify(node.UUID, uuid => work(uuid))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}//}}}
|
|
||||||
getParentWithNextSibling(node) {//{{{
|
|
||||||
let currNode = node
|
|
||||||
while (currNode !== null && currNode.UUID !== ROOT_NODE && currNode.getSiblingAfter() === null) {
|
|
||||||
currNode = currNode.getParent()
|
|
||||||
}
|
|
||||||
return currNode?.getSiblingAfter()
|
|
||||||
}//}}}
|
|
||||||
getLastExpandedNode(node) {//{{{
|
|
||||||
let currNode = node
|
|
||||||
while (this.getNodeExpanded(currNode.UUID) && currNode.hasChildren()) {
|
|
||||||
currNode = currNode.Children[currNode.Children.length - 1]
|
|
||||||
}
|
|
||||||
return currNode
|
|
||||||
}//}}}
|
|
||||||
|
|
||||||
async recursiveExpand(node, state) {//{{{
|
|
||||||
if (state)
|
|
||||||
await this.setNodeExpanded(node, true)
|
|
||||||
|
|
||||||
for (const child of node.Children)
|
|
||||||
await this.recursiveExpand(child, state)
|
|
||||||
|
|
||||||
if (!state)
|
|
||||||
await this.setNodeExpanded(node, false)
|
|
||||||
}//}}}
|
|
||||||
|
|
||||||
async keyHandler(event) {//{{{
|
|
||||||
let handled = true
|
|
||||||
const n = this.selectedNode
|
|
||||||
const Space = ' '
|
|
||||||
|
|
||||||
// This handler would otherwise react to stuff like Ctrl+L.
|
|
||||||
if (event.ctrlKey || event.altKey)
|
|
||||||
return
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
// Space and enter is toggling expansion.
|
|
||||||
// Holding shift down does it recursively.
|
|
||||||
case Space:
|
|
||||||
case 'Enter':
|
|
||||||
const expanded = this.getNodeExpanded(n.UUID)
|
|
||||||
if (event.shiftKey) {
|
|
||||||
this.recursiveExpand(n, !expanded)
|
|
||||||
} else {
|
|
||||||
this.setNodeExpanded(n, !expanded)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'g':
|
|
||||||
case 'Home':
|
|
||||||
this.navigateTop()
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'G':
|
|
||||||
case 'End':
|
|
||||||
this.navigateBottom()
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'j':
|
|
||||||
case 'ArrowDown':
|
|
||||||
await this.navigateDown(this.selectedNode)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'k':
|
|
||||||
case 'ArrowUp':
|
|
||||||
await this.navigateUp(this.selectedNode)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'h':
|
|
||||||
case 'ArrowLeft':
|
|
||||||
await this.navigateLeft(this.selectedNode)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'l':
|
|
||||||
case 'ArrowRight':
|
|
||||||
await this.navigateRight(this.selectedNode)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
// nonsole.log(event.key)
|
|
||||||
handled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handled) {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
}
|
|
||||||
}//}}}
|
|
||||||
async navigateLeft(n) {//{{{
|
|
||||||
if (n === null)
|
|
||||||
return
|
|
||||||
|
|
||||||
const expanded = this.getNodeExpanded(n.UUID)
|
|
||||||
if (expanded && n.hasChildren()) {
|
|
||||||
this.setNodeExpanded(n, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) {
|
|
||||||
await _notes2.current.goToNode(n.getParent()?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const siblingBefore = n.getSiblingBefore()
|
|
||||||
const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
|
|
||||||
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
|
||||||
const siblingAbove = this.getLastExpandedNode(siblingBefore)
|
|
||||||
await _notes2.current.goToNode(siblingAbove?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await _notes2.current.goToNode(n.getSiblingBefore()?.UUID, true, true)
|
|
||||||
}//}}}
|
|
||||||
async navigateRight(n) {//{{{
|
|
||||||
if (n === null)
|
|
||||||
return
|
|
||||||
|
|
||||||
const siblingAfter = n.getSiblingAfter()
|
|
||||||
const expanded = this.getNodeExpanded(n.UUID)
|
|
||||||
|
|
||||||
if (!expanded && n.hasChildren()) {
|
|
||||||
this.setNodeExpanded(n, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expanded && n.hasChildren()) {
|
|
||||||
await _notes2.current.goToNode(n.Children[0]?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n.isLastSibling()) {
|
|
||||||
const nextNode = this.getParentWithNextSibling(n)
|
|
||||||
await _notes2.current.goToNode(nextNode?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true)
|
|
||||||
}//}}}
|
|
||||||
async navigateUp(n) {//{{{
|
|
||||||
if (n === null)
|
|
||||||
return
|
|
||||||
|
|
||||||
let parent = null
|
|
||||||
const siblingBefore = n.getSiblingBefore()
|
|
||||||
let siblingExpanded = false
|
|
||||||
if (siblingBefore !== null)
|
|
||||||
siblingExpanded = this.getNodeExpanded(siblingBefore.UUID)
|
|
||||||
|
|
||||||
if (n.isFirstSibling()) {
|
|
||||||
parent = n.getParent()
|
|
||||||
if (parent?.UUID === ROOT_NODE)
|
|
||||||
return
|
|
||||||
await _notes2.current.goToNode(parent?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
|
||||||
await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (siblingBefore) {
|
|
||||||
await _notes2.current.goToNode(siblingBefore.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}//}}}
|
|
||||||
async navigateDown(n) {//{{{
|
|
||||||
if (n === null)
|
|
||||||
return
|
|
||||||
|
|
||||||
const nodeExpanded = this.getNodeExpanded(n.UUID)
|
|
||||||
|
|
||||||
// Last node, not expanded, so it matters not whether it has children or not.
|
|
||||||
// Traverse upward to nearest parent with next sibling.
|
|
||||||
if (!nodeExpanded && n.isLastSibling()) {
|
|
||||||
const wantedNode = this.getParentWithNextSibling(n)
|
|
||||||
await _notes2.current.goToNode(wantedNode?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
|
|
||||||
const wantedNode = this.getParentWithNextSibling(n)
|
|
||||||
await _notes2.current.goToNode(wantedNode?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node not expanded. Go to this node's next sibling.
|
|
||||||
// GoToNode will abort if given null.
|
|
||||||
if (!nodeExpanded || !n.hasChildren()) {
|
|
||||||
await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node is expanded.
|
|
||||||
// Children will be visually beneath this node, if any.
|
|
||||||
if (nodeExpanded && n.hasChildren()) {
|
|
||||||
await _notes2.current.goToNode(n.Children[0].UUID, true, true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}//}}}
|
|
||||||
async navigateTop() {//{{{
|
|
||||||
const root = await nodeStore.get(ROOT_NODE)
|
|
||||||
if (root.Children.length === 0)
|
|
||||||
return
|
|
||||||
await _notes2.current.goToNode(root.Children[0]?.UUID, true, true)
|
|
||||||
}//}}}
|
|
||||||
async navigateBottom() {//{{{
|
|
||||||
const root = await nodeStore.get(ROOT_NODE)
|
|
||||||
if (root.Children.length === 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
const toplevel = root.Children[root.Children.length - 1]
|
|
||||||
const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID)
|
|
||||||
|
|
||||||
if (toplevelExpanded) {
|
|
||||||
const lastnode = this.getLastExpandedNode(toplevel)
|
|
||||||
await _notes2.current.goToNode(lastnode?.UUID, true, true)
|
|
||||||
} else
|
|
||||||
await _notes2.current.goToNode(root.Children[root.Children.length - 1]?.UUID, true, true)
|
|
||||||
}//}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TreeNode extends Component {
|
|
||||||
constructor(props) {//{{{
|
|
||||||
super(props)
|
|
||||||
this.children_populated = signal(false)
|
|
||||||
if (this.props.node.Level === 0 || this.props.tree.getNodeExpanded(this.props.node.UUID))
|
|
||||||
this.fetchChildren()
|
|
||||||
}//}}}
|
|
||||||
render({ tree, node, parent }) {//{{{
|
|
||||||
// Fetch the next level of children if the parent tree node is expanded and our children thus will be visible.
|
|
||||||
const selected = tree.isSelected(node) ? 'selected' : ''
|
|
||||||
|
|
||||||
if (!this.children_populated.value && tree.getNodeExpanded(parent?.props.node.UUID))
|
|
||||||
this.fetchChildren()
|
|
||||||
|
|
||||||
const children = node.Children.map(node => {
|
|
||||||
tree.treeNodeComponents[node.UUID] = createRef()
|
|
||||||
return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${tree} node=${node} parent=${this} ref=${tree.treeNodeComponents[node.UUID]} selected=${node.UUID === tree.props.app.startNode?.UUID} />`
|
|
||||||
})
|
|
||||||
|
|
||||||
let expandImg = ''
|
|
||||||
if (node.Children.length === 0)
|
|
||||||
expandImg = html`<img src="/images/${window._VERSION}/leaf.svg" />`
|
|
||||||
else {
|
|
||||||
if (tree.getNodeExpanded(node.UUID))
|
|
||||||
expandImg = html`<img src="/images/${window._VERSION}/expanded.svg" />`
|
|
||||||
else
|
|
||||||
expandImg = html`<img src="/images/${window._VERSION}/collapsed.svg" />`
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="node">
|
|
||||||
<div class="expand-toggle" onclick=${() => { tree.setNodeExpanded(node, !tree.getNodeExpanded(node.UUID)) }}>${expandImg}</div>
|
|
||||||
<div class="name ${selected}" onclick=${() => window._notes2.current.goToNode(node.UUID)}>${node.get('Name')}</div>
|
|
||||||
<div class="children ${node.Children.length > 0 && tree.getNodeExpanded(node.UUID) ? 'expanded' : 'collapsed'}">${children}</div>
|
|
||||||
</div>`
|
|
||||||
}//}}}
|
|
||||||
async fetchChildren() {//{{{
|
|
||||||
await this.props.node.fetchChildren()
|
|
||||||
this.children_populated.value = true
|
|
||||||
}//}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Op {
|
class Op {
|
||||||
constructor(id) {
|
constructor(id) {
|
||||||
this.id = id
|
this.id = id
|
||||||
|
|
|
||||||
|
|
@ -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 { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||||
import { MarkedPosition } from './marked_position.mjs'
|
import { MarkedPosition } from './marked_position.mjs'
|
||||||
|
|
||||||
export class N2NodeUI extends CustomHTMLElement {
|
export class N2PageNodeUI extends CustomHTMLElement {
|
||||||
static {// {{{
|
static {// {{{
|
||||||
this.tmpl = document.createElement('template')
|
this.tmpl = document.createElement('template')
|
||||||
this.tmpl.innerHTML = `
|
this.tmpl.innerHTML = `
|
||||||
|
|
@ -72,6 +72,7 @@ export class N2NodeUI extends CustomHTMLElement {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
|
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.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown()))
|
||||||
|
|
||||||
this.showMarkdown(true)
|
this.showMarkdown(true)
|
||||||
|
|
@ -118,6 +119,47 @@ export class N2NodeUI extends CustomHTMLElement {
|
||||||
return this.classList.contains('show-markdown')
|
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) {// {{{
|
editMarkdown(data) {// {{{
|
||||||
this.showMarkdown(false)
|
this.showMarkdown(false)
|
||||||
this.elNodeContent.selectionStart = data.position.start
|
this.elNodeContent.selectionStart = data.position.start
|
||||||
|
|
@ -125,7 +167,7 @@ export class N2NodeUI extends CustomHTMLElement {
|
||||||
this.elNodeContent.focus()
|
this.elNodeContent.focus()
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
customElements.define('n2-nodeui', N2NodeUI)
|
customElements.define('n2-nodeui', N2PageNodeUI)
|
||||||
|
|
||||||
export class Node {
|
export class Node {
|
||||||
static sort(a, b) {//{{{
|
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
|
// vim: foldmethod=marker
|
||||||
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() {//{{{
|
async run() {//{{{
|
||||||
|
// XXX - Delete me
|
||||||
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let duration = 0 // in ms
|
let duration = 0 // in ms
|
||||||
|
|
||||||
|
|
@ -163,13 +166,13 @@ export class Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class N2SyncProgress extends CustomHTMLElement {
|
export class N2SyncProgress extends CustomHTMLElement {
|
||||||
static {
|
static {// {{{
|
||||||
this.tmpl = document.createElement('template')
|
this.tmpl = document.createElement('template')
|
||||||
this.tmpl.innerHTML = `
|
this.tmpl.innerHTML = `
|
||||||
<progress data-el="progress" min=0 max=137 value=0></progress>
|
<progress data-el="progress" min=0 max=137 value=0></progress>
|
||||||
<div data-el="count" class="count">0 / 0</div>
|
<div data-el="count" class="count">0 / 0</div>
|
||||||
`
|
`
|
||||||
}
|
}// }}}
|
||||||
constructor() {//{{{
|
constructor() {//{{{
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
import { ROOT_NODE } from 'node_store'
|
import { ROOT_NODE } from 'node_store'
|
||||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||||
|
import { Color, Solver } from './lib/css_colorize.mjs'
|
||||||
|
|
||||||
export class N2Tree extends CustomHTMLElement {
|
export class N2Tree extends CustomHTMLElement {
|
||||||
static {// {{{
|
static {// {{{
|
||||||
this.tmpl = document.createElement('template')
|
this.tmpl = document.createElement('template')
|
||||||
this.tmpl.innerHTML = `
|
this.tmpl.innerHTML = `
|
||||||
<div data-el="logo" id="logo"><img src="/images/${_VERSION}/logo.svg" /></div>
|
<div data-el="logo" id="logo">
|
||||||
<div class="icons">
|
<img src="/images/${_VERSION}/logo_small.svg" />
|
||||||
|
<img src="/images/${_VERSION}/logo.svg" />
|
||||||
<img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
|
<img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
|
||||||
<img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" />
|
</div>
|
||||||
|
<div class="icons">
|
||||||
|
<img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" />
|
||||||
|
<img data-el="settings" class='settings' src="/images/${_VERSION}/icon_settings.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div data-el="treenodes"></div>
|
<div data-el="treenodes"></div>
|
||||||
`
|
`
|
||||||
|
|
@ -31,7 +36,7 @@ export class N2Tree extends CustomHTMLElement {
|
||||||
this.elSync.addEventListener('click', () => _sync.run())
|
this.elSync.addEventListener('click', () => _sync.run())
|
||||||
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
|
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
|
||||||
|
|
||||||
_mbus.subscribe('NODE_MODIFIED', ({ detail })=>{
|
_mbus.subscribe('NODE_MODIFIED', ({ detail }) => {
|
||||||
const node = detail.data.node
|
const node = detail.data.node
|
||||||
const treenode = this.treeNodeComponents[node.get('UUID')]
|
const treenode = this.treeNodeComponents[node.get('UUID')]
|
||||||
|
|
||||||
|
|
@ -43,6 +48,12 @@ export class N2Tree extends CustomHTMLElement {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.populateFirstLevel()
|
this.populateFirstLevel()
|
||||||
|
|
||||||
|
/* XXX - set color */
|
||||||
|
let color = new Color(255, 96, 80)
|
||||||
|
let solver = new Solver(color)
|
||||||
|
let result = solver.solve()
|
||||||
|
this.elSettings.style.filter = result.filter
|
||||||
}// }}}
|
}// }}}
|
||||||
render() {// {{{
|
render() {// {{{
|
||||||
if (this.rendered)
|
if (this.rendered)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ const CACHED_ASSETS = [
|
||||||
'/js/{{ .VERSION }}/lib/sjcl.js',
|
'/js/{{ .VERSION }}/lib/sjcl.js',
|
||||||
'/js/{{ .VERSION }}/marked_position.mjs',
|
'/js/{{ .VERSION }}/marked_position.mjs',
|
||||||
'/js/{{ .VERSION }}/mbus.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 }}/node_store.mjs',
|
||||||
'/js/{{ .VERSION }}/notes2.mjs',
|
'/js/{{ .VERSION }}/notes2.mjs',
|
||||||
'/js/{{ .VERSION }}/sync.mjs',
|
'/js/{{ .VERSION }}/sync.mjs',
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"checklist": "/js/{{ .VERSION }}/checklist.mjs",
|
"checklist": "/js/{{ .VERSION }}/checklist.mjs",
|
||||||
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
|
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
|
||||||
"node_store": "/js/{{ .VERSION }}/node_store.mjs",
|
"node_store": "/js/{{ .VERSION }}/node_store.mjs",
|
||||||
"node": "/js/{{ .VERSION }}/node.mjs",
|
"node": "/js/{{ .VERSION }}/page_node.mjs",
|
||||||
"tree": "/js/{{ .VERSION }}/tree.mjs"
|
"tree": "/js/{{ .VERSION }}/tree.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,34 @@
|
||||||
{{ define "page" }}
|
{{ define "page" }}
|
||||||
<div id="notes2">
|
<div id="notes2">
|
||||||
<div id="tree" tabindex=0></div>
|
<div id="tree" tabindex=0></div>
|
||||||
<div id="crumbs"></div>
|
|
||||||
<n2-syncprogress></n2-syncprogress>
|
<div id="main-page">
|
||||||
<n2-nodeui id="note"></n2-nodeui>
|
<!-- Storage stats -->
|
||||||
|
<div id="page-storage">
|
||||||
|
<n2-pagestorage></n2-pagestorage>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Node editing -->
|
||||||
|
<div id="page-node">
|
||||||
|
<div id="crumbs"></div>
|
||||||
|
<n2-syncprogress></n2-syncprogress>
|
||||||
|
<n2-nodeui id="note"></n2-nodeui>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import {NodeStore} from 'node_store'
|
|
||||||
|
|
||||||
|
import {NodeStore} from '/js/{{ .VERSION }}/node_store.mjs'
|
||||||
|
|
||||||
import {App} from "/js/{{ .VERSION }}/app.mjs"
|
import {App} from "/js/{{ .VERSION }}/app.mjs"
|
||||||
import {API} from 'api'
|
import {API} from 'api'
|
||||||
import {Sync} from 'sync'
|
import {Sync} from 'sync'
|
||||||
|
import { } from '/js/{{ .VERSION }}/page_storage.mjs'
|
||||||
|
import { } from '/js/{{ .VERSION }}/file.mjs'
|
||||||
|
|
||||||
window.Sync = Sync
|
window.Sync = Sync
|
||||||
|
|
||||||
|
|
|
||||||