import { 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], this.idx) td.appendChild(input) }) 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" input.checked = value getValue = () => input.checked const hotone = this.table.CONSTRAINTS[field.key]?.type == "hotone" if (listeners && 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") 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) }) input.value = value default: break } 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 } } } export default class TracksTable { OPTIONS = { "language": getLanguageOptions } 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 = {} } 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) } }