feat: add toolbar + notifications
This commit is contained in:
parent
2afecd1c04
commit
bc5371de71
@ -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>
|
@ -8,7 +8,14 @@
|
|||||||
<script src="/static/js/index.js"></script>
|
<script src="/static/js/index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
<h1>Metadata Editor</h1>
|
<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>
|
<select name="file-sel" id="file-sel" oninput="selectFile(event)"></select>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user