refactor: split editor in JS modules
This commit is contained in:
parent
d19ab90f38
commit
82d02cfe76
@ -4,9 +4,9 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Edit</title>
|
<title>Edit</title>
|
||||||
<link rel="stylesheet" href="/base.css">
|
<link rel="stylesheet" href="/static/css/base.css">
|
||||||
<link rel="stylesheet" href="index.css">
|
<link rel="stylesheet" href="/static/css/edit.css">
|
||||||
<script src="index.js"></script>
|
<script src="/static/js/edit.mjs" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header id="topbar">
|
<header id="topbar">
|
||||||
@ -44,5 +44,18 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<div class="popup" id="subtitle-integrity">
|
||||||
|
<div class="container">
|
||||||
|
<h3 class="title">Subtitle Track Integrity</h3>
|
||||||
|
<p id="st-integrity-desc"></p>
|
||||||
|
<div class="original">
|
||||||
|
<label for="st-integrity-og">Original Value</label>
|
||||||
|
<input type="text" disabled id="st-integrity-og">
|
||||||
|
</div>
|
||||||
|
<div class="correction">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,231 +0,0 @@
|
|||||||
/** @type TracksTable */
|
|
||||||
let audioTable
|
|
||||||
|
|
||||||
/** @type TracksTable */
|
|
||||||
let subtitleTable
|
|
||||||
|
|
||||||
let data = {}
|
|
||||||
|
|
||||||
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"]
|
|
||||||
}
|
|
||||||
CONSTRAINTS = {
|
|
||||||
"flags/default": {
|
|
||||||
type: "hotone"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(table, callback) {
|
|
||||||
this.table = table
|
|
||||||
this.headers = this.table.querySelector("thead tr")
|
|
||||||
this.body = this.table.querySelector("tbody")
|
|
||||||
this.fields = []
|
|
||||||
this.tracks = []
|
|
||||||
this.callback = callback
|
|
||||||
this.hotones = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
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], i)
|
|
||||||
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, trackIdx) {
|
|
||||||
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
|
|
||||||
const hotone = this.CONSTRAINTS[field.key]?.type == "hotone"
|
|
||||||
|
|
||||||
if (hotone) {
|
|
||||||
if (value) {
|
|
||||||
if (field.key in this.hotones) {
|
|
||||||
alert("Error in metadata file: field ${field.name} is hotone but multiple tracks are enabled ")
|
|
||||||
}
|
|
||||||
this.hotones[field.key] = input
|
|
||||||
}
|
|
||||||
input.addEventListener("click", e => {
|
|
||||||
console.log("HOTONE")
|
|
||||||
if (!input.checked) {
|
|
||||||
console.log("Already checked, preventing uncheck")
|
|
||||||
e.preventDefault()
|
|
||||||
} else {
|
|
||||||
if (field.key in this.hotones) {
|
|
||||||
this.hotones[field.key].checked = false
|
|
||||||
this.hotones[field.key].dispatchEvent(new Event("change"))
|
|
||||||
}
|
|
||||||
this.hotones[field.key] = input
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
input.addEventListener("change", () => {
|
|
||||||
this.callback(trackIdx, field.key, input.checked)
|
|
||||||
})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
input.dataset.key = field.key
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchData(filename) {
|
|
||||||
fetch(`/api/file/${filename}`).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}).then(res => {
|
|
||||||
if (res !== null) {
|
|
||||||
data = res
|
|
||||||
displayData()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayData() {
|
|
||||||
document.getElementById("title").value = data.title
|
|
||||||
|
|
||||||
audioTable.showTracks(data.audio_tracks)
|
|
||||||
subtitleTable.showTracks(data.subtitle_tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDirty() {
|
|
||||||
dirty = true
|
|
||||||
document.getElementById("unsaved").classList.add("show")
|
|
||||||
}
|
|
||||||
|
|
||||||
function save(filename) {
|
|
||||||
fetch(`/api/file/${filename}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
dirty = false
|
|
||||||
document.getElementById("unsaved").classList.remove("show")
|
|
||||||
} else {
|
|
||||||
alert(`Error ${res.status}: ${res.statusText}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function editTrack(listKey, trackIdx, key, value) {
|
|
||||||
const keyParts = key.split("/")
|
|
||||||
let obj = data[listKey][trackIdx]
|
|
||||||
for (const part of keyParts.slice(0, -1)) {
|
|
||||||
obj = obj[part]
|
|
||||||
}
|
|
||||||
obj[keyParts[keyParts.length - 1]] = value
|
|
||||||
setDirty()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
audioTable = new TracksTable(document.getElementById("audio-tracks"), (i, key, value) => {
|
|
||||||
editTrack("audio_tracks", i, key, value)
|
|
||||||
})
|
|
||||||
subtitleTable = new TracksTable(document.getElementById("subtitle-tracks"), (i, key, value) => {
|
|
||||||
editTrack("subtitle_tracks", i, key, value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
const file = params.get("f")
|
|
||||||
document.getElementById("filename").innerText = file
|
|
||||||
|
|
||||||
fetchData(file)
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener("keydown", e => {
|
|
||||||
if (e.key === "s" && e.ctrlKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
const file = params.get("f")
|
|
||||||
save(file)
|
|
||||||
}
|
|
||||||
})
|
|
@ -4,7 +4,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Metadata Editor</title>
|
<title>Metadata Editor</title>
|
||||||
<script src="index.js"></script>
|
<link rel="stylesheet" href="/static/css/base.css">
|
||||||
|
<script src="/static/js/index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Metadata Editor</h1>
|
<h1>Metadata Editor</h1>
|
||||||
|
@ -66,3 +66,28 @@ input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: #5c5c5c5c;
|
||||||
|
|
||||||
|
&:not(.show) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 1.2em;
|
||||||
|
border-radius: 0.4em;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8em;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
editor/public/static/js/edit.mjs
Normal file
6
editor/public/static/js/edit.mjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Editor from "./editor.mjs";
|
||||||
|
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
const editor = new Editor()
|
||||||
|
window.editor = editor
|
||||||
|
})
|
85
editor/public/static/js/editor.mjs
Normal file
85
editor/public/static/js/editor.mjs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import TracksTable from "./tracks_table.mjs"
|
||||||
|
import IntegrityManager from "./integrity_manager.mjs"
|
||||||
|
|
||||||
|
export default class Editor {
|
||||||
|
constructor() {
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
this.filename = params.get("f")
|
||||||
|
window.addEventListener("keydown", e => {
|
||||||
|
if (e.key === "s" && e.ctrlKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.tables = {
|
||||||
|
audio: new TracksTable(this, "audio", "audio-tracks", "audio_tracks"),
|
||||||
|
subtitle: new TracksTable(this, "subtitle", "subtitle-tracks", "subtitle_tracks")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data = {}
|
||||||
|
this.dirty = false
|
||||||
|
|
||||||
|
this.integrity_mgr = new IntegrityManager(this)
|
||||||
|
|
||||||
|
this.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
document.getElementById("filename").innerText = this.filename
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData() {
|
||||||
|
fetch(`/api/file/${this.filename}`).then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).then(res => {
|
||||||
|
if (res !== null) {
|
||||||
|
this.data = res
|
||||||
|
this.displayData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
displayData() {
|
||||||
|
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() {
|
||||||
|
fetch(`/api/file/${this.filename}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(this.data),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.dirty = false
|
||||||
|
document.getElementById("unsaved").classList.remove("show")
|
||||||
|
} else {
|
||||||
|
alert(`Error ${res.status}: ${res.statusText}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirty() {
|
||||||
|
this.dirty = true
|
||||||
|
document.getElementById("unsaved").classList.add("show")
|
||||||
|
}
|
||||||
|
|
||||||
|
editTrack(listKey, trackIdx, key, value) {
|
||||||
|
const keyParts = key.split("/")
|
||||||
|
let obj = this.data[listKey][trackIdx]
|
||||||
|
for (const part of keyParts.slice(0, -1)) {
|
||||||
|
obj = obj[part]
|
||||||
|
}
|
||||||
|
obj[keyParts[keyParts.length - 1]] = value
|
||||||
|
this.setDirty()
|
||||||
|
}
|
||||||
|
}
|
83
editor/public/static/js/integrity_manager.mjs
Normal file
83
editor/public/static/js/integrity_manager.mjs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Track } from "./tracks_table.mjs"
|
||||||
|
import { findLanguage, isLanguageAlias, flattenObj } from "./utils.mjs"
|
||||||
|
|
||||||
|
class Mismatch {
|
||||||
|
constructor(track, key, value) {
|
||||||
|
this.track = track
|
||||||
|
this.key = key
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class IntegrityManager {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('./editor.mjs').default} editor
|
||||||
|
*/
|
||||||
|
constructor(editor) {
|
||||||
|
this.editor = editor
|
||||||
|
this.mismatches = []
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIntegrity() {
|
||||||
|
for (const table of Object.values(this.editor.tables)) {
|
||||||
|
this.checkTableIntegrity(table)
|
||||||
|
}
|
||||||
|
console.log(this.mismatches)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('./tracks_table.mjs').default} table
|
||||||
|
*/
|
||||||
|
checkTableIntegrity(table) {
|
||||||
|
for (const track of table.tracks) {
|
||||||
|
this.checkTrackIntegrity(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Track} track
|
||||||
|
*/
|
||||||
|
checkTrackIntegrity(track) {
|
||||||
|
let fields = this.parseName(track.table.type, track.fields["name"])
|
||||||
|
fields = flattenObj(fields)
|
||||||
|
|
||||||
|
Object.entries(fields).map(([key, value]) => {
|
||||||
|
let equal = track.fields[key] === value
|
||||||
|
if (key === "language") {
|
||||||
|
equal = isLanguageAlias(value, track.fields[key])
|
||||||
|
}
|
||||||
|
if (!equal) {
|
||||||
|
this.addMismatchField(track, key, value)
|
||||||
|
console.error(`Mismatch for field ${key}:\n- name: ${value}\n- track: ${track.fields[key]}`)
|
||||||
|
} else {
|
||||||
|
track.fields[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
parseName(trackType, name) {
|
||||||
|
const lower = name.toLowerCase()
|
||||||
|
const parts = lower.split(/\b/)
|
||||||
|
const fields = {flags: {}}
|
||||||
|
switch (trackType) {
|
||||||
|
case "subtitle":
|
||||||
|
let forced = parts.includes("forced")
|
||||||
|
let lang = findLanguage(lower)
|
||||||
|
if (forced) {fields.flags.forced = forced}
|
||||||
|
if (lang !== null) {fields.language = lang}
|
||||||
|
let ad = parts.includes("sdh") || parts.includes("ad")
|
||||||
|
if (ad) {fields.flags.hearing_impaired = ad}
|
||||||
|
let original = parts.includes("vo")
|
||||||
|
if (original) {fields.flags.original = original}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
addMismatchField(track, key, value) {
|
||||||
|
this.mismatches.push(new Mismatch(track, key, value))
|
||||||
|
}
|
||||||
|
}
|
172
editor/public/static/js/tracks_table.mjs
Normal file
172
editor/public/static/js/tracks_table.mjs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { flattenObj } from "./utils.mjs"
|
||||||
|
|
||||||
|
export class Track {
|
||||||
|
constructor(table, idx, fields) {
|
||||||
|
this.table = table
|
||||||
|
this.idx = idx
|
||||||
|
this.fields = flattenObj(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRow() {
|
||||||
|
const tr = document.createElement("tr")
|
||||||
|
tr.dataset.i = this.idx
|
||||||
|
this.table.fields.forEach(field => {
|
||||||
|
const td = tr.insertCell(-1)
|
||||||
|
const input = this.makeInput(field, this.fields[field.key], this.idx)
|
||||||
|
td.appendChild(input)
|
||||||
|
})
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|
||||||
|
makeInput(field, value) {
|
||||||
|
let input = document.createElement("input")
|
||||||
|
let getValue = () => input.value
|
||||||
|
switch (field.type) {
|
||||||
|
case "num":
|
||||||
|
input.type = "number"
|
||||||
|
input.value = value
|
||||||
|
getValue = () => +input.value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "str":
|
||||||
|
input.type = "text"
|
||||||
|
input.value = value
|
||||||
|
break
|
||||||
|
|
||||||
|
case "bool":
|
||||||
|
input.type = "checkbox"
|
||||||
|
input.checked = value
|
||||||
|
getValue = () => input.checked
|
||||||
|
const hotone = this.table.CONSTRAINTS[field.key]?.type == "hotone"
|
||||||
|
|
||||||
|
if (hotone) {
|
||||||
|
if (value) {
|
||||||
|
if (field.key in this.table.hotones) {
|
||||||
|
alert(`Error in metadata file: field ${field.name} is hotone but multiple tracks are enabled`)
|
||||||
|
}
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
this.table.hotones[field.key] = input
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "sel":
|
||||||
|
input = document.createElement("select")
|
||||||
|
const options = this.table.OPTIONS[field.key]
|
||||||
|
options.forEach(option => {
|
||||||
|
const opt = document.createElement("option")
|
||||||
|
opt.innerText = option
|
||||||
|
opt.value = option
|
||||||
|
input.appendChild(opt)
|
||||||
|
})
|
||||||
|
input.value = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
input.dataset.key = field.key
|
||||||
|
if (this.table.CONSTRAINTS[field.key]?.type === "readonly") {
|
||||||
|
input.disabled = true
|
||||||
|
}
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
this.editValue(field.key, getValue())
|
||||||
|
})
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
editValue(key, value) {
|
||||||
|
this.fields[key] = value
|
||||||
|
this.table.editTrack(this.idx, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TracksTable {
|
||||||
|
OPTIONS = {
|
||||||
|
"language": ["fre", "eng"]
|
||||||
|
}
|
||||||
|
CONSTRAINTS = {
|
||||||
|
"flags/default": {
|
||||||
|
type: "hotone"
|
||||||
|
},
|
||||||
|
"index": {
|
||||||
|
type: "readonly"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
type: "readonly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('./editor.mjs').default} editor The parent editor
|
||||||
|
* @param {string} type The type of tracks. One of `['audio', 'subtitle']`
|
||||||
|
* @param {string} tableId The id of the table element
|
||||||
|
* @param {string} dataKey The key of the tracks list inside of the data object
|
||||||
|
*/
|
||||||
|
constructor(editor, type, tableId, dataKey) {
|
||||||
|
this.editor = editor
|
||||||
|
this.type = type
|
||||||
|
this.table = document.getElementById(tableId)
|
||||||
|
this.headers = this.table.querySelector("thead tr")
|
||||||
|
this.body = this.table.querySelector("tbody")
|
||||||
|
this.fields = []
|
||||||
|
this.tracks = []
|
||||||
|
this.dataKey = dataKey
|
||||||
|
this.hotones = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTracks(tracks) {
|
||||||
|
this.tracks = tracks.map((t, i) => new Track(this, i, t))
|
||||||
|
this.clear()
|
||||||
|
if (tracks.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.detectFields()
|
||||||
|
this.addHeaders()
|
||||||
|
this.tracks.forEach(track => {
|
||||||
|
this.body.appendChild(track.makeRow())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.headers.innerHTML = ""
|
||||||
|
this.body.innerHTML = ""
|
||||||
|
this.fields = []
|
||||||
|
}
|
||||||
|
|
||||||
|
detectFields() {
|
||||||
|
Object.entries(this.tracks[0].fields).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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
editTrack(trackIdx, fieldKey, fieldValue) {
|
||||||
|
this.editor.editTrack(this.dataKey, trackIdx, fieldKey, fieldValue)
|
||||||
|
}
|
||||||
|
}
|
48
editor/public/static/js/utils.mjs
Normal file
48
editor/public/static/js/utils.mjs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Flattens an object recursively. Nested keys are joined with slashes ('/')
|
||||||
|
* @param {object} obj The object to flatten
|
||||||
|
* @returns {object} The flattened object
|
||||||
|
*/
|
||||||
|
export 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LANGUAGES = {
|
||||||
|
"fr": ["fre", "fra", "french", "francais", "français", "vf", "vff"],
|
||||||
|
"fr-ca": ["vfq", "quebec", "québec"],
|
||||||
|
"en": ["eng", "ang", "english", "anglais"],
|
||||||
|
"de": ["deu", "ger", "german", "allemand"],
|
||||||
|
"ko": ["kor", "cor", "korean", "coreen", "coréen"],
|
||||||
|
"ja": ["jap", "japanese", "japonais"]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to find a language name in the given string
|
||||||
|
* @param {string} value The string in which to search for a language
|
||||||
|
* @returns {?string} The language key if it could be determined, null otherwise
|
||||||
|
*/
|
||||||
|
export function findLanguage(value) {
|
||||||
|
for (const lang in LANGUAGES) {
|
||||||
|
const aliases = [lang].concat(LANGUAGES[lang])
|
||||||
|
const matches = aliases.map(a => new RegExp("\\b" + a + "\\b").test(value)).some(v => v)
|
||||||
|
if (matches) {
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLanguageAlias(langKey, value) {
|
||||||
|
return [langKey].concat(LANGUAGES[langKey]).includes(value)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user