import { findLanguage, flattenObj, getLanguageOptions } from "./utils.mjs" export class Track { constructor(table, idx, fields) { /** @type {TracksTable} */ this.table = table /** @type {number} */ this.idx = idx /** @type {object} */ this.fields = flattenObj(fields) this.row = null } makeRow() { this.row = document.createElement("tr") this.row.dataset.i = this.idx this.table.fields.forEach(field => { const td = this.row.insertCell(-1) const input = this.makeInput(field, this.fields[field.key]) td.appendChild(input) if (field.key === "name") { const btn = document.getElementById("improve-btn").cloneNode(true) btn.id = null btn.classList.remove("template") btn.addEventListener("click", () => { this.improveName() btn.classList.add("clicked") setTimeout(() => btn.classList.remove("clicked"), 1000) }) td.appendChild(btn) } }) return this.row } makeInput(field, value, listeners=true) { 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" getValue = () => input.checked const onehot = this.table.CONSTRAINTS[field.key]?.type == "onehot" if (listeners && onehot) { if (value) { 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 } } input.addEventListener("click", e => { if (!input.checked) { e.preventDefault() } else { 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.onehots[field.key] = input } }) } input.checked = value break case "sel": input = document.createElement("select") let options = this.table.OPTIONS[field.key] if (typeof options === "function") { options = options() } options.forEach(option => { const opt = document.createElement("option") opt.innerText = option.display 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 } if (listeners) { input.addEventListener("change", () => { this.editValue(field.key, getValue()) }) } return input } editValue(key, value) { this.fields[key] = value this.table.editTrack(this.idx, key, value) const input = this.row.querySelector(`[data-key='${key}']`) const fieldType = this.table.getFieldProps(key).type switch (fieldType) { case "bool": input.checked = value break default: input.value = value break } } improveName() { this.table.editor.integrity_mgr.improveName(this) } } export default class TracksTable { OPTIONS = { "language": getLanguageOptions } CONSTRAINTS = { "flags/default": { type: "onehot" }, "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.onehots = {} } getFieldProps(key) { return this.fields.find(f => f.key == key) } 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) } }