feat: add web editor base
This commit is contained in:
parent
3c4cc4b331
commit
79b3a2196c
9
editor/public/base.css
Normal file
9
editor/public/base.css
Normal file
@ -0,0 +1,9 @@
|
||||
* {
|
||||
/*padding: 0;*/
|
||||
/*margin: 0;*/
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Ubuntu;
|
||||
}
|
13
editor/public/edit/index.css
Normal file
13
editor/public/edit/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
#filename {
|
||||
font-size: 80%;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#unsaved {
|
||||
color: #9d4916;
|
||||
font-size: 80%;
|
||||
|
||||
&:not(.show) {
|
||||
display: none;
|
||||
}
|
||||
}
|
48
editor/public/edit/index.html
Normal file
48
editor/public/edit/index.html
Normal file
@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Edit</title>
|
||||
<link rel="stylesheet" href="/base.css">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header id="topbar">
|
||||
<nav>
|
||||
|
||||
</nav>
|
||||
</header>
|
||||
<aside id="toolbar">
|
||||
|
||||
</aside>
|
||||
<main id="main">
|
||||
<h1>Edit <code id="filename"></code><span id="unsaved"> - Unsaved</span></h1>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<label for="title">Title</label>
|
||||
<input type="text" id="title" name="title">
|
||||
</div>
|
||||
</div>
|
||||
<div class="audio">
|
||||
<h3>Audio Tracks</h3>
|
||||
<table id="audio-tracks">
|
||||
<thead>
|
||||
<tr></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table">
|
||||
<h3>Subtitle Tracks</h3>
|
||||
<table id="subtitle-tracks">
|
||||
<thead>
|
||||
<tr></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
157
editor/public/edit/index.js
Normal file
157
editor/public/edit/index.js
Normal file
@ -0,0 +1,157 @@
|
||||
/** @type TracksTable */
|
||||
let audioTable
|
||||
|
||||
/** @type TracksTable */
|
||||
let subtitleTable
|
||||
|
||||
function flattenObj(obj) {
|
||||
const res = {}
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if (typeof value === "object") {
|
||||
value = flattenObj(value)
|
||||
Object.entries(value).forEach(([key2, value2]) => {
|
||||
res[key + "/" + key2] = value2
|
||||
})
|
||||
} else {
|
||||
res[key] = value
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
class TracksTable {
|
||||
OPTIONS = {
|
||||
"language": ["fre", "eng"]
|
||||
}
|
||||
|
||||
constructor(table) {
|
||||
this.table = table
|
||||
this.headers = this.table.querySelector("thead tr")
|
||||
this.body = this.table.querySelector("tbody")
|
||||
this.fields = []
|
||||
this.tracks = []
|
||||
}
|
||||
|
||||
showTracks(tracks) {
|
||||
this.tracks = tracks.map(flattenObj)
|
||||
this.clear()
|
||||
if (tracks.length === 0) {
|
||||
return
|
||||
}
|
||||
this.detectFields()
|
||||
this.addHeaders()
|
||||
this.tracks.forEach((track, i) => {
|
||||
const tr = document.createElement("tr")
|
||||
tr.dataset.i = i
|
||||
this.fields.forEach(field => {
|
||||
const td = tr.insertCell(-1)
|
||||
const input = this.makeInput(field, track[field.key])
|
||||
td.appendChild(input)
|
||||
})
|
||||
this.body.appendChild(tr)
|
||||
})
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.headers.innerHTML = ""
|
||||
this.body.innerHTML = ""
|
||||
this.fields = []
|
||||
}
|
||||
|
||||
detectFields() {
|
||||
Object.entries(this.tracks[0]).forEach(([key, value]) => {
|
||||
let type = {
|
||||
boolean: "bool",
|
||||
number: "num"
|
||||
}[typeof value] ?? "str"
|
||||
if (key === "language") {
|
||||
type = "sel"
|
||||
}
|
||||
|
||||
const name = key.split("/").slice(-1)[0]
|
||||
this.fields.push({name, type, key})
|
||||
})
|
||||
}
|
||||
|
||||
addHeaders() {
|
||||
this.fields.forEach(field => {
|
||||
const th = document.createElement("th")
|
||||
th.innerText = field.name
|
||||
this.headers.appendChild(th)
|
||||
})
|
||||
}
|
||||
|
||||
makeInput(field, value) {
|
||||
let input = document.createElement("input")
|
||||
switch (field.type) {
|
||||
case "num":
|
||||
input.type = "number"
|
||||
input.value = value
|
||||
break
|
||||
|
||||
case "str":
|
||||
input.type = "text"
|
||||
input.value = value
|
||||
break
|
||||
|
||||
case "bool":
|
||||
input.type = "checkbox"
|
||||
input.checked = value
|
||||
break
|
||||
|
||||
case "sel":
|
||||
input = document.createElement("select")
|
||||
const options = this.OPTIONS[field.name]
|
||||
options.forEach(option => {
|
||||
const opt = document.createElement("option")
|
||||
opt.innerText = option
|
||||
opt.value = option
|
||||
input.appendChild(opt)
|
||||
})
|
||||
input.value = value
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
function fetchData(filename) {
|
||||
fetch("/api/file", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
file: filename
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(res => {
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
}
|
||||
return null
|
||||
}).then(res => {
|
||||
if (res !== null) {
|
||||
displayData(res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function displayData(data) {
|
||||
document.getElementById("title").value = data.title
|
||||
|
||||
audioTable.showTracks(data.audio_tracks)
|
||||
subtitleTable.showTracks(data.subtitle_tracks)
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
audioTable = new TracksTable(document.getElementById("audio-tracks"))
|
||||
subtitleTable = new TracksTable(document.getElementById("subtitle-tracks"))
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const file = params.get("f")
|
||||
document.getElementById("filename").innerText = file
|
||||
|
||||
fetchData(file)
|
||||
})
|
13
editor/public/index.html
Normal file
13
editor/public/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Metadata Editor</title>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Metadata Editor</h1>
|
||||
<select name="file-sel" id="file-sel" oninput="selectFile(event)"></select>
|
||||
</body>
|
||||
</html>
|
31
editor/public/index.js
Normal file
31
editor/public/index.js
Normal file
@ -0,0 +1,31 @@
|
||||
function addOptions(files) {
|
||||
const select = document.getElementById("file-sel")
|
||||
select.innerHTML = ""
|
||||
const defaultOpt = document.createElement("option")
|
||||
defaultOpt.innerText = "----- Select a file -----"
|
||||
defaultOpt.value = ""
|
||||
select.appendChild(defaultOpt)
|
||||
files.forEach(file => {
|
||||
const option = document.createElement("option")
|
||||
option.innerText = file
|
||||
option.value = file
|
||||
select.appendChild(option)
|
||||
})
|
||||
}
|
||||
|
||||
function selectFile(event) {
|
||||
const file = event.target.value
|
||||
if (file !== "") {
|
||||
const url = new URL("/edit/", window.location.origin)
|
||||
url.searchParams.set("f", file)
|
||||
window.location.href = url.href
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
fetch("/api/files").then(res => {
|
||||
return res.json()
|
||||
}).then(files => {
|
||||
addOptions(files)
|
||||
})
|
||||
})
|
90
editor/server.py
Normal file
90
editor/server.py
Normal file
@ -0,0 +1,90 @@
|
||||
from http import HTTPStatus
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
import json
|
||||
import os
|
||||
import socketserver
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
PORT = 8000
|
||||
|
||||
class MyHandler(SimpleHTTPRequestHandler):
|
||||
DATA_DIR = "metadata"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(
|
||||
*args,
|
||||
directory="public",
|
||||
**kwargs
|
||||
)
|
||||
self.query: dict = {}
|
||||
self.data: Optional[dict|list] = None
|
||||
|
||||
def read_body_data(self):
|
||||
self.log_message("Reading body data")
|
||||
try:
|
||||
raw_data = self.rfile.read(int(self.headers["Content-Length"]))
|
||||
self.data = json.loads(raw_data)
|
||||
except:
|
||||
self.send_error(HTTPStatus.NOT_ACCEPTABLE, "Malformed JSON body")
|
||||
self.log_error("Malformed JSON body")
|
||||
return False
|
||||
return True
|
||||
|
||||
def do_GET(self):
|
||||
self.query = parse_qs(urlparse(self.path).query)
|
||||
if self.path.startswith("/api/"):
|
||||
self.handle_api_get(self.path.removeprefix("/api/").removesuffix("/"))
|
||||
return
|
||||
super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
self.query = parse_qs(urlparse(self.path).query)
|
||||
if self.path.startswith("/api/"):
|
||||
self.handle_api_post(self.path.removeprefix("/api/").removesuffix("/"))
|
||||
return
|
||||
self.send_error(HTTPStatus.NOT_FOUND)
|
||||
|
||||
def handle_api_get(self, path: str):
|
||||
print(f"API request at {path}")
|
||||
if path == "files":
|
||||
files: list[str] = self.get_files()
|
||||
self.send_json(files)
|
||||
return
|
||||
|
||||
def handle_api_post(self, path: str):
|
||||
if path == "file":
|
||||
if self.read_body_data():
|
||||
data = self.get_file(self.data["file"])
|
||||
if data is None:
|
||||
self.send_error(HTTPStatus.NOT_FOUND)
|
||||
self.log_message("File not found")
|
||||
else:
|
||||
self.log_message("Got file")
|
||||
self.send_json(data)
|
||||
|
||||
def send_json(self, data: dict|list):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode("utf-8"))
|
||||
|
||||
def get_files(self):
|
||||
return os.listdir(self.DATA_DIR)
|
||||
|
||||
def get_file(self, filename: str) -> Optional[dict|list]:
|
||||
if filename not in self.get_files():
|
||||
return None
|
||||
with open(os.path.join(self.DATA_DIR, filename), "r") as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
|
||||
print(f"Serving on port {PORT}")
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
x
Reference in New Issue
Block a user