From bc5371de71be1d95990d30f5813124c440cf970a Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Wed, 30 Apr 2025 15:34:21 +0200 Subject: [PATCH] feat: add toolbar + notifications --- editor/public/edit/index.html | 25 +++-- editor/public/index.html | 11 ++- editor/public/static/css/base.css | 98 ++++++++++++++++++- editor/public/static/css/edit.css | 51 ++++++++++ editor/public/static/js/editor.mjs | 54 +++++++++- editor/public/static/js/integrity_manager.mjs | 22 ++++- editor/public/static/js/tracks_table.mjs | 55 ++++++++--- editor/public/static/js/utils.mjs | 4 +- 8 files changed, 285 insertions(+), 35 deletions(-) diff --git a/editor/public/edit/index.html b/editor/public/edit/index.html index 93261d7..696fa3e 100644 --- a/editor/public/edit/index.html +++ b/editor/public/edit/index.html @@ -13,20 +13,20 @@ -
- +
+ Home + + + + +
-
-

Edit - Unsaved

+

Editing - Unsaved

- +
@@ -99,5 +99,12 @@
+ + +
\ No newline at end of file diff --git a/editor/public/index.html b/editor/public/index.html index 02317a6..63b9471 100644 --- a/editor/public/index.html +++ b/editor/public/index.html @@ -8,7 +8,14 @@ -

Metadata Editor

- +
+

Metadata Editor

+
+
+
+ + +
+
\ No newline at end of file diff --git a/editor/public/static/css/base.css b/editor/public/static/css/base.css index e229348..3ba2c02 100644 --- a/editor/public/static/css/base.css +++ b/editor/public/static/css/base.css @@ -1,13 +1,107 @@ * { - /*padding: 0;*/ - /*margin: 0;*/ + margin: 0; box-sizing: border-box; } +html, body { + height: 100%; +} + body { font-family: Ubuntu; + margin: 0; + padding: 0; } .template { display: none !important; +} + +main { + padding: 1.2em; +} + +header { + background-color: #2b2b2b; + padding: 1.2em; + grid-area: header; + display: flex; + gap: 0.8em; + color: white; + + a, button { + padding: 0.4em 0.8em; + border: none; + color: black; + background-color: #e4e4e4; + font-size: inherit; + font-family: inherit; + text-decoration: none; + border-radius: 0.2em; + cursor: pointer; + + &:hover { + background-color: #dbdbdb; + } + } +} + +aside { + position: fixed; + right: 0; + top: 0; + bottom: 0; + background-color: white; + border-left: solid black 2px; +} + +.notif { + --bg: #f0f0f0; + --border: #727272; + --fg: black; + --col: #f0f0f0; + + &[data-type="success"] { + --bg: #e0ffe0; + --border: #727f72; + --fg: black; + --col: #8dff8d; + } + + &[data-type="error"] { + --bg: #ffe0e0; + --border: #7f7272; + --fg: black; + --col: #ff8d8d; + } + + &[data-type="warning"] { + --bg: #ffefe0; + --border: #7f7f72; + --fg: black; + --col: #ffc36a; + } +} + +#notifs { + position: fixed; + top: 0; + left: 0; + right: 0; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 0.4em; + padding-top: 0.4em; + width: max-content; + + .notif { + padding: 0.4em 0.8em; + border-radius: 0.6em; + background-color: var(--bg); + border: solid var(--border) 2px; + color: var(--fg); + max-width: 30em; + cursor: pointer; + } } \ No newline at end of file diff --git a/editor/public/static/css/edit.css b/editor/public/static/css/edit.css index b67e06d..0b80eba 100644 --- a/editor/public/static/css/edit.css +++ b/editor/public/static/css/edit.css @@ -1,3 +1,13 @@ +main { + display: flex; + flex-direction: column; + gap: 1.2em; +} + +#toggle-notifs { + margin-left: auto; +} + #filename { font-size: 80%; font-style: italic; @@ -117,6 +127,8 @@ button.improve { transform: translateY(-50%); margin-left: 0.4em; border-radius: 0.4em; + background: none; + &:hover{ background-color: #8d8d8d42; } @@ -318,4 +330,43 @@ button.improve { padding: 0.2em 0.4em; background-color: #fafafa; } +} + +#notifs-hist { + padding: 0.8em; + height: 100%; + display: flex; + flex-direction: column; + + &:not(.show) { + display: none; + } + + #close-notifs { + align-self: flex-end; + background: none; + border: none; + padding: 0.4em 0.8em; + border-radius: 0.2em; + font-family: inherit; + font-size: inherit; + cursor: pointer; + + &:hover { + background-color: #ebebeb; + } + } + + .list { + display: flex; + flex-direction: column; + gap: 0.2em; + overflow-y: auto; + margin-top: 0.6em; + + .notif { + border-left: solid var(--col) 4px; + padding: 0.4em; + } + } } \ No newline at end of file diff --git a/editor/public/static/js/editor.mjs b/editor/public/static/js/editor.mjs index 693b51d..ba62581 100644 --- a/editor/public/static/js/editor.mjs +++ b/editor/public/static/js/editor.mjs @@ -23,6 +23,13 @@ export default class Editor { this.integrity_mgr = new IntegrityManager(this) + document.getElementById("check-integrity").addEventListener("click", () => this.checkIntegrity()) + document.getElementById("improve-all").addEventListener("click", () => this.improveAllNames()) + document.getElementById("save").addEventListener("click", () => this.save()) + document.getElementById("reload").addEventListener("click", () => window.location.reload()) + document.getElementById("toggle-notifs").addEventListener("click", () => this.toggleNotifications()) + document.getElementById("close-notifs").addEventListener("click", () => this.closeNotifications()) + this.setup() } @@ -49,7 +56,6 @@ export default class Editor { document.getElementById("title").value = this.data.title this.tables.audio.loadTracks(this.data.audio_tracks) this.tables.subtitle.loadTracks(this.data.subtitle_tracks) - this.integrity_mgr.checkIntegrity() } save() { @@ -63,8 +69,9 @@ export default class Editor { if (res.ok) { this.dirty = false document.getElementById("unsaved").classList.remove("show") + this.notify("Saved successfully !", "success") } else { - alert(`Error ${res.status}: ${res.statusText}`) + this.notify(`Error ${res.status}: ${res.statusText}`, "error", 10000) } }) } @@ -78,4 +85,47 @@ export default class Editor { updateObjectFromJoinedKey(this.data[listKey][trackIdx], key, value) this.setDirty() } + + notify(text, type, duration=5000) { + const list = document.getElementById("notifs") + const hist = document.getElementById("notifs-hist").querySelector(".list") + const notif = document.createElement("div") + notif.classList.add("notif") + notif.dataset.type = type + notif.innerText = text + list.appendChild(notif) + setTimeout(() => notif.remove(), duration) + notif.addEventListener("click", () => notif.remove()) + hist.prepend(notif.cloneNode(true)) + } + + checkIntegrity() { + if (this.integrity_mgr.checkIntegrity()) { + this.notify("No integrity error detected !", "success") + } + } + + improveAllNames() { + this.integrity_mgr.improveAllNames() + this.notify("Improved all names !", "success") + } + + toggleNotifications() { + const hist = document.getElementById("notifs-hist") + if (hist.classList.contains("show")) { + this.closeNotifications() + } else { + this.openNotifications() + } + } + + openNotifications() { + const hist = document.getElementById("notifs-hist") + hist.classList.add("show") + } + + closeNotifications() { + const hist = document.getElementById("notifs-hist") + hist.classList.remove("show") + } } \ No newline at end of file diff --git a/editor/public/static/js/integrity_manager.mjs b/editor/public/static/js/integrity_manager.mjs index beb9046..cc568c0 100644 --- a/editor/public/static/js/integrity_manager.mjs +++ b/editor/public/static/js/integrity_manager.mjs @@ -78,7 +78,7 @@ class MismatchCorrection { export default class IntegrityManager { IGNORE_KEYS = [ - "type", "channels" + "type", "channels_details" ] /** @@ -136,11 +136,17 @@ export default class IntegrityManager { } checkIntegrity() { + this.ignoreList = [] + this.mismatches = [] for (const table of Object.values(this.editor.tables)) { this.checkTableIntegrity(table) } + if (this.mismatches.length === 0) { + return true + } this.nextError() + return false } /** @@ -198,7 +204,7 @@ export default class IntegrityManager { const channels = lower.match(/\d+\.\d+/) if (channels) { - fields.channels = channels[0] + fields.channels_details = channels[0] } break @@ -297,8 +303,8 @@ export default class IntegrityManager { if (fields.flags.visual_impaired) { name += " AD" } - if (fields.channels) { - name += " / " + fields.channels + if (fields.channels_details) { + name += " / " + fields.channels_details } break case "subtitle": @@ -345,6 +351,14 @@ export default class IntegrityManager { } return input } + + improveAllNames() { + for (const table of Object.values(this.editor.tables)) { + for (const track of table.tracks) { + this.improveName(track) + } + } + } /** * diff --git a/editor/public/static/js/tracks_table.mjs b/editor/public/static/js/tracks_table.mjs index 7caeec3..34474b2 100644 --- a/editor/public/static/js/tracks_table.mjs +++ b/editor/public/static/js/tracks_table.mjs @@ -1,4 +1,4 @@ -import { flattenObj, getLanguageOptions } from "./utils.mjs" +import { findLanguage, flattenObj, getLanguageOptions } from "./utils.mjs" export class Track { constructor(table, idx, fields) { @@ -54,29 +54,36 @@ export class Track { case "bool": input.type = "checkbox" - input.checked = value - getValue = () => input.checked - const hotone = this.table.CONSTRAINTS[field.key]?.type == "hotone" - if (listeners && hotone) { + getValue = () => input.checked + const onehot = this.table.CONSTRAINTS[field.key]?.type == "onehot" + + if (listeners && onehot) { if (value) { - if (field.key in this.table.hotones) { - alert(`Error in metadata file: field ${field.name} is hotone but multiple tracks are enabled`) + if (field.key in this.table.onehots) { + this.table.editor.notify( + `Error in metadata file: field '${field.name}' is onehot but multiple tracks are enabled. Only the first one will be enabled`, + "error", + 20000 + ) + value = false + } else { + this.table.onehots[field.key] = input } - this.table.hotones[field.key] = input } input.addEventListener("click", e => { if (!input.checked) { e.preventDefault() } else { - if (field.key in this.table.hotones) { - this.table.hotones[field.key].checked = false - this.table.hotones[field.key].dispatchEvent(new Event("change")) + if (field.key in this.table.onehots) { + this.table.onehots[field.key].checked = false + this.table.onehots[field.key].dispatchEvent(new Event("change")) } - this.table.hotones[field.key] = input + this.table.onehots[field.key] = input } }) } + input.checked = value break case "sel": @@ -91,11 +98,31 @@ export class Track { opt.value = option.value input.appendChild(opt) }) + + if (field.key === "language") { + const lang = findLanguage(value) + if (lang === null) { + this.table.editor.notify( + `Unknown language '${value}' for ${this.table.type} track ${this.idx}`, + "error", + 20000 + ) + } else if (lang !== value) { + this.table.editor.notify( + `Language of ${this.table.type} track ${this.idx} was corrected (${value} -> ${lang})`, + "warning" + ) + value = lang + } + } + + input.value = value default: break } + input.name = field.key + "[]" input.dataset.key = field.key if (this.table.CONSTRAINTS[field.key]?.type === "readonly") { input.disabled = true @@ -135,7 +162,7 @@ export default class TracksTable { } CONSTRAINTS = { "flags/default": { - type: "hotone" + type: "onehot" }, "index": { type: "readonly" @@ -160,7 +187,7 @@ export default class TracksTable { this.fields = [] this.tracks = [] this.dataKey = dataKey - this.hotones = {} + this.onehots = {} } getFieldProps(key) { diff --git a/editor/public/static/js/utils.mjs b/editor/public/static/js/utils.mjs index ea7a606..e771bbb 100644 --- a/editor/public/static/js/utils.mjs +++ b/editor/public/static/js/utils.mjs @@ -75,10 +75,10 @@ export function getLanguageAliases(langTag) { export function findLanguage(value) { for (const lang in LANGUAGES) { const aliases = getLanguageAliases(lang) - const matches = aliases.map(a => { + const matches = aliases.some(a => { return new RegExp("\\b" + a + "\\b").test(value) }) - if (matches.some(v => v)) { + if (matches) { return lang } }