feat: add toolbar + notifications

This commit is contained in:
Louis Heredero 2025-04-30 15:34:21 +02:00
parent 2afecd1c04
commit bc5371de71
Signed by: HEL
GPG Key ID: 8D83DE470F8544E7
8 changed files with 285 additions and 35 deletions

View File

@ -13,20 +13,20 @@
<img src="/static/images/improve.svg"> <img src="/static/images/improve.svg">
<img class="clicked" src="/static/images/improve_clicked.svg"> <img class="clicked" src="/static/images/improve_clicked.svg">
</button> </button>
<header id="topbar"> <header id="toolbar">
<nav> <a href="/">Home</a>
<button id="check-integrity">Check integrity</button>
</nav> <button id="improve-all">Improve all names</button>
<button id="save">Save</button>
<button id="reload">Reload</button>
<button id="toggle-notifs">Notifications</button>
</header> </header>
<aside id="toolbar">
</aside>
<main id="main"> <main id="main">
<h1>Edit <code id="filename"></code><span id="unsaved"> - Unsaved</span></h1> <h1>Editing <code id="filename"></code><span id="unsaved"> - Unsaved</span></h1>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<label for="title">Title</label> <label for="title">Title</label>
<input type="text" id="title" name="title"> <input type="text" id="title" name="title" size="50">
</div> </div>
</div> </div>
<div class="audio"> <div class="audio">
@ -99,5 +99,12 @@
</div> </div>
</form> </form>
</div> </div>
<aside id="notifs-hist">
<button id="close-notifs">Close</button>
<h2>Notifications</h2>
<div class="list"></div>
</aside>
<div id="notifs"></div>
</body> </body>
</html> </html>

View File

@ -8,7 +8,14 @@
<script src="/static/js/index.js"></script> <script src="/static/js/index.js"></script>
</head> </head>
<body> <body>
<h1>Metadata Editor</h1> <header>
<select name="file-sel" id="file-sel" oninput="selectFile(event)"></select> <h1>Metadata Editor</h1>
</header>
<main>
<div>
<label for="file-sel">Choose a file</label>
<select name="file-sel" id="file-sel" oninput="selectFile(event)"></select>
</div>
</main>
</body> </body>
</html> </html>

View File

@ -1,13 +1,107 @@
* { * {
/*padding: 0;*/ margin: 0;
/*margin: 0;*/
box-sizing: border-box; box-sizing: border-box;
} }
html, body {
height: 100%;
}
body { body {
font-family: Ubuntu; font-family: Ubuntu;
margin: 0;
padding: 0;
} }
.template { .template {
display: none !important; 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;
}
}

View File

@ -1,3 +1,13 @@
main {
display: flex;
flex-direction: column;
gap: 1.2em;
}
#toggle-notifs {
margin-left: auto;
}
#filename { #filename {
font-size: 80%; font-size: 80%;
font-style: italic; font-style: italic;
@ -117,6 +127,8 @@ button.improve {
transform: translateY(-50%); transform: translateY(-50%);
margin-left: 0.4em; margin-left: 0.4em;
border-radius: 0.4em; border-radius: 0.4em;
background: none;
&:hover{ &:hover{
background-color: #8d8d8d42; background-color: #8d8d8d42;
} }
@ -319,3 +331,42 @@ button.improve {
background-color: #fafafa; 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;
}
}
}

View File

@ -23,6 +23,13 @@ export default class Editor {
this.integrity_mgr = new IntegrityManager(this) 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() this.setup()
} }
@ -49,7 +56,6 @@ export default class Editor {
document.getElementById("title").value = this.data.title document.getElementById("title").value = this.data.title
this.tables.audio.loadTracks(this.data.audio_tracks) this.tables.audio.loadTracks(this.data.audio_tracks)
this.tables.subtitle.loadTracks(this.data.subtitle_tracks) this.tables.subtitle.loadTracks(this.data.subtitle_tracks)
this.integrity_mgr.checkIntegrity()
} }
save() { save() {
@ -63,8 +69,9 @@ export default class Editor {
if (res.ok) { if (res.ok) {
this.dirty = false this.dirty = false
document.getElementById("unsaved").classList.remove("show") document.getElementById("unsaved").classList.remove("show")
this.notify("Saved successfully !", "success")
} else { } 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) updateObjectFromJoinedKey(this.data[listKey][trackIdx], key, value)
this.setDirty() 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")
}
} }

View File

@ -78,7 +78,7 @@ class MismatchCorrection {
export default class IntegrityManager { export default class IntegrityManager {
IGNORE_KEYS = [ IGNORE_KEYS = [
"type", "channels" "type", "channels_details"
] ]
/** /**
@ -136,11 +136,17 @@ export default class IntegrityManager {
} }
checkIntegrity() { checkIntegrity() {
this.ignoreList = []
this.mismatches = []
for (const table of Object.values(this.editor.tables)) { for (const table of Object.values(this.editor.tables)) {
this.checkTableIntegrity(table) this.checkTableIntegrity(table)
} }
if (this.mismatches.length === 0) {
return true
}
this.nextError() this.nextError()
return false
} }
/** /**
@ -198,7 +204,7 @@ export default class IntegrityManager {
const channels = lower.match(/\d+\.\d+/) const channels = lower.match(/\d+\.\d+/)
if (channels) { if (channels) {
fields.channels = channels[0] fields.channels_details = channels[0]
} }
break break
@ -297,8 +303,8 @@ export default class IntegrityManager {
if (fields.flags.visual_impaired) { if (fields.flags.visual_impaired) {
name += " AD" name += " AD"
} }
if (fields.channels) { if (fields.channels_details) {
name += " / " + fields.channels name += " / " + fields.channels_details
} }
break break
case "subtitle": case "subtitle":
@ -346,6 +352,14 @@ export default class IntegrityManager {
return input return input
} }
improveAllNames() {
for (const table of Object.values(this.editor.tables)) {
for (const track of table.tracks) {
this.improveName(track)
}
}
}
/** /**
* *
* @param {Track} track * @param {Track} track

View File

@ -1,4 +1,4 @@
import { flattenObj, getLanguageOptions } from "./utils.mjs" import { findLanguage, flattenObj, getLanguageOptions } from "./utils.mjs"
export class Track { export class Track {
constructor(table, idx, fields) { constructor(table, idx, fields) {
@ -54,29 +54,36 @@ export class Track {
case "bool": case "bool":
input.type = "checkbox" 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 (value) {
if (field.key in this.table.hotones) { if (field.key in this.table.onehots) {
alert(`Error in metadata file: field ${field.name} is hotone but multiple tracks are enabled`) 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 => { input.addEventListener("click", e => {
if (!input.checked) { if (!input.checked) {
e.preventDefault() e.preventDefault()
} else { } else {
if (field.key in this.table.hotones) { if (field.key in this.table.onehots) {
this.table.hotones[field.key].checked = false this.table.onehots[field.key].checked = false
this.table.hotones[field.key].dispatchEvent(new Event("change")) 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 break
case "sel": case "sel":
@ -91,11 +98,31 @@ export class Track {
opt.value = option.value opt.value = option.value
input.appendChild(opt) 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 input.value = value
default: default:
break break
} }
input.name = field.key + "[]"
input.dataset.key = field.key input.dataset.key = field.key
if (this.table.CONSTRAINTS[field.key]?.type === "readonly") { if (this.table.CONSTRAINTS[field.key]?.type === "readonly") {
input.disabled = true input.disabled = true
@ -135,7 +162,7 @@ export default class TracksTable {
} }
CONSTRAINTS = { CONSTRAINTS = {
"flags/default": { "flags/default": {
type: "hotone" type: "onehot"
}, },
"index": { "index": {
type: "readonly" type: "readonly"
@ -160,7 +187,7 @@ export default class TracksTable {
this.fields = [] this.fields = []
this.tracks = [] this.tracks = []
this.dataKey = dataKey this.dataKey = dataKey
this.hotones = {} this.onehots = {}
} }
getFieldProps(key) { getFieldProps(key) {

View File

@ -75,10 +75,10 @@ export function getLanguageAliases(langTag) {
export function findLanguage(value) { export function findLanguage(value) {
for (const lang in LANGUAGES) { for (const lang in LANGUAGES) {
const aliases = getLanguageAliases(lang) const aliases = getLanguageAliases(lang)
const matches = aliases.map(a => { const matches = aliases.some(a => {
return new RegExp("\\b" + a + "\\b").test(value) return new RegExp("\\b" + a + "\\b").test(value)
}) })
if (matches.some(v => v)) { if (matches) {
return lang return lang
} }
} }