/** * @param {string} type * @param {?{classNames: ?string[], content: ?string, attributes: ?Object., onClick: ?(() => void)}} props * @param {?HTMLElement[]} children * @returns {HTMLElement} */ function element(type, props, children) { let element = document.createElement(type); if (props?.classNames != null && props.classNames.length > 0) { element.classList.add(props.classNames); } if (props?.content != null) { element.innerText = props.content; } if (props?.attributes != null) { for (let attribute in props.attributes) { element.setAttribute(attribute, props.attributes[attribute]); } } if (props?.onClick != null) { element.onclick = props.onClick; } if (children != null) { for (let child of children) { element.appendChild(child); } } return element; } const components = { /** * @param {string} name * @param {Video[]} videos * @returns {HTMLElement} */ series(name, videos) { return element("div", { classNames: ["series"] }, [ element("a", { content: name, attributes: { "href": "/series.html?name=" + encodeURIComponent(name) } }), element("div", { content: [...new Set(videos.map(v => v.metadata.author))].join(", "), classNames: ["series-authors"] }), element("div", { classNames: ["series-videos"] }, videos.map(v => components.video(v))) ]); }, /** * @param {Video} video * @returns {HTMLElement} */ video(video) { return element("a", { classNames: ["video"], attributes: { "href": "/watch.html?id=" + encodeURIComponent(video.id), "target": "_blank" } }, [ element("img", { attributes: { "src": API_URL + "/library/video/" + encodeURIComponent(video.id) + "/thumbnail" } }), element("span", { content: video.metadata.title }), ]); }, /** * @param {Video} video * @returns {HTMLElement} */ async player(video) { const streamURL = API_URL + "/library/video/" + encodeURIComponent(video.id) + "/stream"; const downloadVideo = () => { window.location.href = streamURL; }; return element("div", { classNames: ["player"] }, [ element("div", { content: video.metadata.title, classNames: ["player-title"] }), element("div", { content: video.metadata.author, classNames: ["player-author"] }), element("video", { attributes: { "poster": API_URL + "/library/video/" + encodeURIComponent(video.id) + "/thumbnail", "controls": "" } }, [ element("source", { attributes: { "src": streamURL } }) ]), element("button", { onClick: downloadVideo }, [ element("img", { attributes: { "src": "/icon/download.svg", "width": "24", "height": "24" } }), element("span", { content: "Download" }), ]), await components.videoMetadata(video) ]); }, /** * @param {Video} video * @returns {HTMLElement} */ async videoMetadata(video) { let metadata; try { metadata = await api.getVideoMetadata(video.id); } catch (e) { showError(e); return element("span", { content: "Failed to load" }); } let form = element("form", { classNames: ["video-metadata-table"] }, [ ...components.metadataInput(metadata, "Title", "title"), ...components.metadataInput(metadata, "Series", "series"), ...components.metadataInput(metadata, "Author", "author"), ...components.metadataInput(metadata, "Index", "index"), ]); let updateMetadata = async () => { let formData = new FormData(form); let metadata = {}; formData.forEach((v, k) => { if (v != "") { if (k == "index") { // TODO: card coded check, improve v = parseInt(v); } metadata[k] = v; } }); try { await api.updateVideoMetadata(video.id, metadata); window.location.reload(); } catch (e) { showError(e); } }; return element("details", { classNames: ["video-metadata"] }, [ element("summary", { content: "Metadata" }), form, element("button", { content: "Update", onClick: updateMetadata }) ]); }, /** * @param {FullVideoMetadata} metadata * @param {string} label * @param {string} key * @returns {HTMLElement} */ metadataInput(metadata, label, key) { let raw = metadata.raw[key]; let inherited = metadata.inherited[key]; return [ element("span", { content: label }), element("input", { attributes: { "name": key, "value": raw ?? "", "placeholder": "(inherited: " + inherited + ")" } }), ]; }, /** * @param {any} error * @returns {HTMLElement} */ error(error) { return element("div", { classNames: ["error"] }, [ element("b", { content: "Oops, an error occurred" }), element("span", { content: errorToString(error) }), element("button", { content: "Reload", onClick: () => window.location.reload() }) ]); } }