Begin adding WebUI

See #10
This commit is contained in:
Joshua Ramon Enslin 2025-03-02 22:59:51 +01:00
parent 5abc1fe75a
commit a8a6292d2b
Signed by: jrenslin
GPG Key ID: 46016F84501B70AE
15 changed files with 1022 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import (
"slices"
"os"
"gitea.armuli.eu/museum-digital/museum-digital-webdav-uploader/src/cli"
"gitea.armuli.eu/museum-digital/museum-digital-webdav-uploader/src/webui"
)
// Attempts to connect to WebDAV server, verifying that the authentication data
@ -54,6 +55,8 @@ func main() {
cli.ListLocalMetadata()
} else if slices.Contains(os.Args, "--local-list-media") {
cli.ListLocalMedia()
} else if slices.Contains(os.Args, "--webui") {
webui.Run()
} else {
// cli.RunManualSetup()
}

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>museum-digital uploader</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<link rel="shortcut icon" sizes="16x16 32x32" href="/logo.svg" />
<link rel="shortcut icon" sizes="64x64" href="/logo.svg" />
<link rel="apple-touch-icon" sizes="256x256" href="/logo.svg" />
<meta name="theme-color" content="#FFF" />
<link rel="stylesheet" type="text/css" href="/md-uploader.css" />
</head>
<body>
<header>
<img src="/logo.svg">
<h1>museum-digital:uploader</h1>
</header>
<div id="wrap">
<script type='text/javascript' src='/md-uploader.js' async></script>
</body>
</html>

View File

@ -0,0 +1,423 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1020"
height="1020"
id="svg2985"
version="1.1"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="logo-md-code-black.svg"
inkscape:export-filename="mdlogo-code-512px.png"
inkscape:export-xdpi="48.189999"
inkscape:export-ydpi="48.189999"
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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title42">General logo for museum-digital (black background)</title>
<defs
id="defs2987" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#ffffff"
borderopacity="0.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="0"
inkscape:zoom="0.98994949"
inkscape:cx="375.27167"
inkscape:cy="658.11439"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="3840"
inkscape:window-height="2076"
inkscape:window-x="0"
inkscape:window-y="-33"
inkscape:window-maximized="1"
fit-margin-top="5"
fit-margin-left="5"
fit-margin-right="5"
fit-margin-bottom="5"
showguides="false"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata2990">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by/4.0/" />
<dc:title>General logo for museum-digital (black background)</dc:title>
<dc:date>2019</dc:date>
<dc:creator>
<cc:Agent>
<dc:title>museum-digital</dc:title>
</cc:Agent>
</dc:creator>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(8,-40.362183)">
<rect
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:8.82220364;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect845"
width="1011.1779"
height="1011.1778"
x="-3.5888982"
y="44.773335"
ry="0" />
<flowRoot
xml:space="preserve"
id="flowRoot3763"
style="font-style:normal;font-weight:normal;line-height:0.01%;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"
transform="matrix(1.0379746,0,0,1.0379746,-169.56958,-12.773255)"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997"><flowRegion
id="flowRegion3765"
style="font-family:sans-serif;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"><rect
id="rect3767"
width="1008.1323"
height="1000.051"
x="-6.0609155"
y="50.290859"
style="font-family:sans-serif;text-align:end;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1" /></flowRegion><flowPara
id="flowPara3769"
style="font-size:40px;line-height:1.25;font-family:sans-serif;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"> </flowPara></flowRoot>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-stretch:normal;font-size:12.4557px;line-height:0%;font-family:'Bookman Old Style';-inkscape-font-specification:'Bookman Old Style Semi-Light';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.03797;stroke-opacity:1"
x="348.29947"
y="659.89093"
id="text3771"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833"
width="79.41317"
height="79.41317"
x="98.490639"
y="742.45844"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-3"
width="79.413177"
height="79.41317"
x="98.490639"
y="626.96552"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6"
width="79.41317"
height="79.41317"
x="98.490639"
y="511.47241"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:10.21611977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-3-7"
width="82.178253"
height="47.530357"
x="97.108063"
y="429.24481"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-5"
width="79.413162"
height="79.41317"
x="271.73004"
y="742.45844"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-3-3"
width="79.41317"
height="79.41317"
x="271.73004"
y="626.96552"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5"
width="79.413162"
height="79.41317"
x="271.73004"
y="511.47241"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-5-6"
width="79.413162"
height="79.41317"
x="444.96948"
y="742.45844"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-3-3-2"
width="79.41317"
height="79.41317"
x="444.96948"
y="626.96545"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9"
width="79.413162"
height="79.41317"
x="444.96948"
y="511.47235"
rx="0"
ry="39.706581"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:10.21612072;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-1"
width="47.530357"
height="82.178246"
x="201.05171"
y="510.08987"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:10.21612072;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-1-2"
width="47.530357"
height="82.178246"
x="374.29114"
y="510.08987"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7"
width="79.413162"
height="79.41317"
x="595.11035"
y="742.45844"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-0"
width="79.413162"
height="79.41317"
x="710.60327"
y="742.45844"
rx="0"
ry="0"
inkscape:transform-center-x="-9.9165682"
inkscape:transform-center-y="47.832846"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-9"
width="79.413162"
height="79.41317"
x="826.09625"
y="742.45844"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-3"
width="79.413162"
height="79.41317"
x="595.11035"
y="626.96552"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-3-6"
width="79.413162"
height="79.41317"
x="595.11035"
y="511.47241"
rx="0"
ry="39.706581"
inkscape:transform-center-x="-29.166407"
inkscape:transform-center-y="139.41528"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-3-6-0"
width="79.413162"
height="79.41317"
x="710.60327"
y="511.47241"
rx="0"
ry="0"
inkscape:transform-center-x="-29.166375"
inkscape:transform-center-y="139.41528"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-6"
width="79.413162"
height="79.41317"
x="826.09625"
y="626.96552"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-3-2"
width="79.413162"
height="79.41317"
x="826.09625"
y="511.47241"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-3-6-6"
width="79.413162"
height="79.41317"
x="826.09625"
y="395.97949"
rx="0"
ry="0"
inkscape:transform-center-x="-29.166404"
inkscape:transform-center-y="139.4153"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:12.98119068;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-3-6-6-1"
width="79.413162"
height="79.41317"
x="826.09625"
y="280.48651"
rx="0"
ry="0"
inkscape:transform-center-x="-29.166404"
inkscape:transform-center-y="139.41528"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:9.61340618;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-3"
width="41.781116"
height="82.780952"
x="443.28558"
y="509.78845"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:9.61340523;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-75"
width="82.780945"
height="41.78112"
x="443.28558"
y="550.78833"
rx="0"
ry="0"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:9.61340523;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-3-6-3"
width="82.780945"
height="41.78112"
x="593.42645"
y="550.78839"
rx="0"
ry="0"
inkscape:transform-center-x="-30.4033"
inkscape:transform-center-y="73.349637"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:9.61340523;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect833-6-5-9-7-3-6-5"
width="41.781116"
height="82.780952"
x="634.42627"
y="509.78851"
rx="0"
ry="0"
inkscape:transform-center-x="-15.345135"
inkscape:transform-center-y="145.32765"
inkscape:export-xdpi="96.379997"
inkscape:export-ydpi="96.379997" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,97 @@
:root {
--color-bg: #FFF;
--color-bg2: #F5F5F5;
--color-bg-transparent: rgba(255,255,255,.4);
--color-bg-transparent-stronger: rgba(255,255,255,.7);
--color-bg-transparent-dark: rgba(0,0,0,.4);
--color-fg: #000;
--color-fg2: #212121;
--color-bg-dark: #212121;
--color-fg-medium: #484848;
--color-borders: #D6D6D6;
--color-accent: #546E7A;
--color-accent-hover: #455A64;
--color-accent-focus: #78909C;
--color-accent-fg: #FFF;
--color-accent-hover-fg: #FFF;
--color-accent-focus-fg: #FFF;
--color-accent2: #484848;
--color-accent2-fg: #FFF;
--color-red: #EF5350;
--color-green: #4CAF50;
--color-blue: #0288D1;
--def-padding-main: 1em;
--def-border-radius: 6px;
--color-white: #FFF;
--color-black: #000;
}
html {
background:
linear-gradient(135deg,#0000 18.75%,var(--color-bg2) 0 31.25%,#0000 0),
repeating-linear-gradient(45deg,var(--color-bg2) -6.25% 6.25%,var(--color-bg) 0 18.75%);
background-size: 64px 64px;
padding: 0;
font-family: Sans;
}
body { margin: 0; padding: 0; }
header { display: block; background: var(--color-bg); font-size: 1.5em; padding: .4em 2rem;
box-shadow: 0 4px 2px -2px var(--color-borders); }
header * { margin: 0; padding: 0; vertical-align: middle; }
header img { height: 1em; }
h1 { display: inline-block; margin-left: 1em; font-size: .8em; }
#wrap { display: grid; grid-template-columns: 400px 1fr; margin: 1em 2rem; grid-gap: 2em; }
#wrap > * { vertical-align: top; }
aside { background: var(--color-bg); border-radius: 1px solid var(--color-borders); box-shadow: 0 0 6px var(--color-borders); border-radius: 2px; }
h2 { margin: 0; padding: .8rem; font-size: 1.4em;
border-bottom: .15em solid var(--color-borders); }
aside .label-input { display: grid;
grid-template-columns: 150px 2fr; border-bottom: 1px solid var(--color-borders); }
aside .label-input:hover { border-color: var(--color-accent); }
aside .label-input > * { display: inline-block;
font-size: .9em;
padding: .5rem .8rem; border: 0; outline: 0; }
aside label { font-weight: bold; color: var(--color-fg-medium); }
select { background: transparent; }
/***
* Slider
* Thanks: mrhyddenn
* The following CSS slider is licensed under the MIT license, 2025 mrhyddenn.
* https://uiverse.io/mrhyddenn/old-fish-66
* */
/* The switch - the box around the slider */
.switch { font-size: 17px; position: relative; display: inline-block; width: 62px; height: 35px; }
/* Hide default HTML checkbox */
.switch input { opacity: 1; width: 0; height: 0;
}
/* The slider */
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0px;
background: var(--color-bg); transition: .4s; border-radius: 30px; border: 1px solid var(--color-borders);
}
.slider:before { position: absolute; content: "";
height: 1.9em; width: 1.9em; border-radius: 16px; left: 1.2px; top: 0; bottom: 0
background-color: var(--color-bg); box-shadow: 0 2px 5px var(--color-accent-hover); transition: .4s;
}
input:checked + .slider { background-color: var(--color-accent); border: 1px solid transparent;
}
input:checked + .slider:before { transform: translateX(1.5em); background-color: var(--color-accent-focus); }

View File

@ -0,0 +1,183 @@
"use strict";
class App {
lang;
tls;
config;
parserList;
wrap;
aside;
main;
generateTextElement(type, value) {
const elem = document.createElement(type);
elem.textContent = value;
return elem;
}
generateLabelInput(id, labelText, type, value) {
const output = document.createElement("div");
output.classList.add("label-input");
const label = document.createElement("label");
label.textContent = labelText;
label.setAttribute("for", id);
output.appendChild(label);
const input = document.createElement("input");
input.id = id;
input.type = "text";
input.value = value;
output.appendChild(input);
return {'wrap': output, 'input': input};
}
async generateSidebar() {
console.log("hi");
this.aside.appendChild(this.generateTextElement("h2", "Settings")); // TODO
// Set up fields
const instanceLinkArea = this.generateLabelInput("instance-link",
"Instance link", "url", this.config.settings.instance);
this.aside.appendChild(instanceLinkArea.wrap);
const usernameArea = this.generateLabelInput("username",
"Username", "text", this.config.settings.username);
this.aside.appendChild(usernameArea.wrap);
const mailArea = this.generateLabelInput("mail",
"Mail", "email", this.config.settings.mail);
this.aside.appendChild(mailArea.wrap);
const institutionIdArea = this.generateLabelInput("institutionId",
"Institution ID", "number", this.config.settings.institution_id);
this.aside.appendChild(institutionIdArea.wrap);
const parserDiv = document.createElement("div");
parserDiv.classList.add("label-input");
const parserLabel = document.createElement("label");
parserLabel.setAttribute("for", "parserSelect");
parserLabel.textContent = "Parser";
parserDiv.appendChild(parserLabel);
const parserSelect = document.createElement("select");
parserSelect.id = "parserSelect";
for (const entry of this.parserList) {
const opt = document.createElement("option");
opt.value = entry.title;
opt.textContent = entry.title;
if (entry.title === this.config.settings.parser) {
opt.setAttribute("selected", "selected");
}
parserSelect.appendChild(opt);
}
parserDiv.appendChild(parserSelect);
this.aside.appendChild(parserDiv);
const uploadFolderArea = this.generateLabelInput("upload-folder",
"Upload directory", "text", this.config.settings.upload_directory);
this.aside.appendChild(uploadFolderArea.wrap);
const visibleDivOuter = document.createElement("div");
visibleDivOuter.classList.add("label-input");
const visibleLabel = document.createElement("label");
visibleLabel.setAttribute("for", "visible");
visibleLabel.textContent = "Publish immediately?";
visibleDivOuter.appendChild(visibleLabel);
const visibleDiv = document.createElement("div");
const visibleSwitch = document.createElement("div");
visibleSwitch.classList.add("switch");
const visibleInput = document.createElement("input");
visibleInput.type = "checkbox";
if (this.config.settings.visible === true) {
visibleInput.checked = "checked";
console.log(visibleInput);
}
visibleSwitch.appendChild(visibleInput);
const visibleSlider = document.createElement("span");
visibleSlider.classList.add("slider");
visibleSlider.addEventListener('click', function() {
visibleInput.checked = !visibleInput.checked;
console.log(visibleInput);
});
visibleSwitch.appendChild(visibleSlider);
visibleDiv.appendChild(visibleSwitch);
visibleDivOuter.appendChild(visibleDiv);
this.aside.appendChild(visibleDivOuter);
}
generateMain() {
}
render() {
if (this.config.setup_required === true) {
wrap.classList.add("one-column");
this.generateSidebar();
}
else {
this.generateSidebar();
this.generateMain();
}
}
setupFromApis() {
const app = this;
let done = 0;
function wrapFinish() {
if (done === 2) {
app.render();
}
}
window.fetch('/get-settings', {method: 'GET', cache: 'no-cache', credentials: 'same-origin'})
.then(function(response) { return response.json(); })
.then(function(elements) {
app.config = elements;
done++;
wrapFinish();
});
window.fetch('/list-parsers', {method: 'GET', cache: 'no-cache', credentials: 'same-origin'})
.then(function(response) { return response.json(); })
.then(function(elements) {
app.parserList = elements;
done++;
wrapFinish();
});
}
constructor() {
this.wrap = document.getElementById("wrap");
this.lang = navigator.language.substring(0, 2)
this.aside = document.createElement("aside");
this.main = document.createElement("main");
this.wrap.appendChild(this.aside);
this.wrap.appendChild(this.main);
this.setupFromApis();
}
}
new App();

46
src/webui/headers.go Normal file
View File

@ -0,0 +1,46 @@
package webui
import (
"net/http"
)
// Sets generally useful HTTP headers, e.g. limiting frame-options.
func setDefaultHeaders(w http.ResponseWriter) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Strict-Transport-Security", "max-age=63072000")
}
// Sets the HTTP headers expected to be returned when serving HTML pages.
func setHeadersForHtml(w http.ResponseWriter) {
setDefaultHeaders(w)
w.Header().Set("Content-Security-Policy", "default-src 'self' https:; font-src 'self'; object-src 'none'; frame-src 'self'; frame-ancestors 'none'; base-uri 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'")
}
// Sets the HTTP headers expected to be returned when serving JSON.
func setHeadersForJson(w http.ResponseWriter) {
setDefaultHeaders(w)
w.Header().Set("Content-Type", "application/json")
}
// Sets the HTTP headers expected to be returned when serving CSS files.
func setHeadersForCss(w http.ResponseWriter) {
setDefaultHeaders(w)
w.Header().Set("Content-Type", "text/css")
}
// Sets the HTTP headers expected to be returned when serving JS files.
func setHeadersForJs(w http.ResponseWriter) {
setDefaultHeaders(w)
w.Header().Set("Content-Type", "text/javascript")
}
// Sets the HTTP headers expected to be returned when serving SVG files.
func setHeadersForSvg(w http.ResponseWriter) {
setDefaultHeaders(w)
w.Header().Set("Content-Type", "image/svg+xml")
}

View File

@ -0,0 +1,27 @@
package webui
import (
"encoding/json"
"fmt"
"net/http"
"gitea.armuli.eu/museum-digital/museum-digital-webdav-uploader/src/configloader"
)
type getSettingsApiResponse struct {
SetupRequired bool `json:"setup_required"`
Settings configloader.MDWebDavUploaderConfig `json:"settings"`
}
// Generates the API output for listing current settings.
func serveApiGetSettings(w http.ResponseWriter, r *http.Request) {
setHeadersForJson(w)
output := getSettingsApiResponse{SetupRequired: setupRequired, Settings: config}
outputJson, encodeErr := json.Marshal(output)
if encodeErr != nil {
panic("Failed to create JSON")
}
fmt.Fprint(w, string(outputJson))
}

View File

@ -0,0 +1,22 @@
package webui
import (
"encoding/json"
"fmt"
"net/http"
"gitea.armuli.eu/museum-digital/museum-digital-webdav-uploader/src/configloader"
)
// Provides the JSON API for listing all available parsers.
func serveApiListParsers(w http.ResponseWriter, r *http.Request) {
setHeadersForJson(w)
outputJson, encodeErr := json.Marshal(configloader.ListParsers())
if encodeErr != nil {
panic("Failed to create JSON")
}
fmt.Fprint(w, string(outputJson))
}

View File

@ -0,0 +1,36 @@
package webui
import (
"encoding/json"
"fmt"
"net/http"
"gitea.armuli.eu/museum-digital/museum-digital-webdav-uploader/src/uploadsrcdir"
)
type listUploadableApiResponse struct {
Metadata []string `json:"metadata"`
MediaFiles []string `json:"media_files"`
}
// Provides the JSON API for listing all metadata and media files that
// are currently listed to be uploaded.
func serveApiListUploadable(w http.ResponseWriter, r *http.Request) {
setHeadersForJson(w)
if setupRequired == true {
fmt.Fprint(w, "{'metadata': '', 'media_files': []}")
return
}
uploadableMetadata, uploadableMedia := uploadsrcdir.GetUploadableFiles(config)
output := listUploadableApiResponse{Metadata: uploadableMetadata, MediaFiles: uploadableMedia}
outputJson, encodeErr := json.Marshal(output)
if encodeErr != nil {
panic("Failed to create JSON")
}
fmt.Fprint(w, string(outputJson))
}

View File

@ -0,0 +1,59 @@
package webui
import (
"encoding/json"
"fmt"
"net/http"
"gitea.armuli.eu/museum-digital/museum-digital-webdav-uploader/src/configloader"
)
type updateSettingsResponse struct {
Success bool `json:"success"`
ValidationError string `json:"error"`
}
// Provides the API for updating the general configuration.
func serveApiUpdateSettings(w http.ResponseWriter, r *http.Request) {
setHeadersForJson(w)
// Parse POST data
parseErr := r.ParseForm()
if parseErr != nil {
fmt.Println("Failed to load form data")
return
}
// Load new config from a POST variable 'settings' that provides the full
// settings as a JSON-encoded string.
rawInputConfig := r.PostForm.Get("settings")
var newConfig configloader.MDWebDavUploaderConfig
unmarshalErr := json.Unmarshal([]byte(rawInputConfig), &newConfig)
if unmarshalErr != nil {
fmt.Fprint(w, "{\"success\": false, \"error\": \"Invalid input JSON\"}")
return
}
// Attempt to store the new settings. Validation takes place while storing
var output updateSettingsResponse
validationErr := configloader.StoreConfigToFile(newConfig, "")
if validationErr == nil {
output = updateSettingsResponse{Success: true, ValidationError: ""}
// Reload config
config, setupRequired, _ = configloader.LoadFromFile("")
} else {
output = updateSettingsResponse{Success: false, ValidationError: validationErr.Error()}
}
// Format response in JSON, then print it
outputJson, encodeErr := json.Marshal(output)
if encodeErr != nil {
panic("Failed to create JSON")
}
fmt.Fprint(w, string(outputJson))
}

View File

@ -0,0 +1,16 @@
package webui
import (
_ "embed"
"fmt"
"net/http"
)
//go:embed assets/index.htm
var indexHtml string
// Serves the app shell. Contents are managed via APIs and JS.
func serveAppShell(w http.ResponseWriter, r *http.Request) {
setHeadersForHtml(w)
fmt.Fprint(w, indexHtml)
}

15
src/webui/serveCss.go Normal file
View File

@ -0,0 +1,15 @@
package webui
import (
_ "embed"
"fmt"
"net/http"
)
//go:embed assets/md-uploader.css
var webuiCss string
func serveCss(w http.ResponseWriter, r *http.Request) {
setHeadersForCss(w)
fmt.Fprint(w, webuiCss)
}

16
src/webui/serveJs.go Normal file
View File

@ -0,0 +1,16 @@
package webui
import (
_ "embed"
"fmt"
"net/http"
)
//go:embed assets/md-uploader.js
var webuiJs string
// Serves the JS for the web app.
func serveJs(w http.ResponseWriter, r *http.Request) {
setHeadersForJs(w)
fmt.Fprint(w, webuiJs)
}

16
src/webui/serveLogo.go Normal file
View File

@ -0,0 +1,16 @@
package webui
import (
_ "embed"
"fmt"
"net/http"
)
//go:embed assets/logo-md-code-black.svg
var logoSvg string
// Serves the logo.
func serveLogo(w http.ResponseWriter, r *http.Request) {
setHeadersForSvg(w)
fmt.Fprint(w, logoSvg)
}

39
src/webui/server.go Normal file
View File

@ -0,0 +1,39 @@
package webui
import (
"fmt"
"log"
"net/http"
"gitea.armuli.eu/museum-digital/museum-digital-webdav-uploader/src/configloader"
)
var config configloader.MDWebDavUploaderConfig
var setupRequired bool
// Sets up the server and manages routing.
func Run() {
config, setupRequired, _ = configloader.LoadFromFile("")
// Bind callable URLs
http.HandleFunc("/", serveAppShell) // Serve app shell
http.HandleFunc("/get-settings", serveApiGetSettings) // API for listing current settings
http.HandleFunc("/list-uploadable", serveApiListUploadable) // API to list uploadable files
http.HandleFunc("/list-parsers", serveApiListParsers) // API to list available parsers.
http.HandleFunc("/update-settings", serveApiUpdateSettings) // API for updating settings
// http.HandleFunc("/trigger-upload", serveFilePage) // Serve page for specific files
// http.HandleFunc("/contribute.json", ) // Serve page for specific files
http.HandleFunc("/md-uploader.css", serveCss) // Main CSS file.
http.HandleFunc("/md-uploader.js", serveJs) // Main JS file.
http.HandleFunc("/logo.svg", serveLogo) // Logo.
port := "8080"
// Set port to listen on
fmt.Println("Server started at port " + string(port))
err := http.ListenAndServe(":"+port, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}