chore (rewrite): viewerpage refactoring

This commit is contained in:
MickaelK
2023-12-23 16:02:17 +11:00
parent 21bf973ba9
commit b5b7268630
12 changed files with 268 additions and 120 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ export default async function main() {
try {
await Promise.all([ // procedure with no outside dependencies
setup_translation(),
// setup_xdg_open(),
setup_xdg_open(),
// setup_cache(), // TODO: dependency on session
setup_device(),
// setup_sw(), // TODO
@@ -92,7 +92,7 @@ function setup_translation() {
async function setup_xdg_open() {
window.overrides = {};
await loadJS("/overrides/xdg-open.js");
return loadJS(import.meta.url, "/overrides/xdg-open.js");
}
async function setup_device() {
+17 -31
View File
@@ -1,20 +1,3 @@
/* chromecast */
:root {
--disconnected-color: #F2F2F2;
--connected-color: var(--primary);
}
google-cast-launcher {
display: inline-block;
height: 23px;
width: 23px;
cursor: pointer;
padding: 0 5px;
position: relative;
top: 1px;
cursor: pointer;
}
.component_menubar {
background: var(--dark);
color: #f1f1f1;
@@ -63,23 +46,26 @@ google-cast-launcher {
width: 21px;
}
.menubar-appear {
display: inline-block;
opacity: 0;
transform: translateY(2px);
}
.menubar-appear.menubar-appear-active {
opacity: 1;
transform: translateY(0px);
transition: all 0.25s ease-out;
transition-delay: 0.30s;
}
/* icons in additional menu */
.component_menubar .action-item [is="component-dropdown"] .dropdown_button {
border: none;
padding: 2px 0px;
margin: 0;
}
/* chromecast */
:root {
--disconnected-color: #F2F2F2;
--connected-color: var(--primary);
}
google-cast-launcher {
display: inline-block;
height: 23px;
width: 23px;
cursor: pointer;
padding: 0 5px;
position: relative;
top: 1px;
cursor: pointer;
}
+14 -2
View File
@@ -1,10 +1,9 @@
import { createElement } from "../lib/skeleton/index.js";
import { animate, slideYIn } from "../lib/animate.js";
import { basename } from "../lib/path.js";
import assert from "../lib/assert.js";
import { loadCSS } from "../helpers/loader.js";
await loadCSS(import.meta.url, "./menubar.css");
import "./dropdown.js";
export default class ComponentMenubar extends window.HTMLElement {
@@ -26,6 +25,19 @@ export default class ComponentMenubar extends window.HTMLElement {
`;
}
async connectedCallback() {
await loadCSS(import.meta.url, "./menubar.css");
const $title = this.querySelector(".titlebar");
$title.style.opacity = 0;
this.timeoutID = setTimeout(() => {
animate($title, { time: 250, keyframes: slideYIn(2) });
}, 300);
}
disconnectedCallback() {
clearTimeout(this.timeoutID);
}
render($fragment) {
this.querySelector(".action-item").appendChild($fragment);
}
@@ -4,10 +4,12 @@ import { qs } from "../../lib/dom.js";
import { onDestroy } from "../../lib/skeleton/lifecycle.js";
import { loadCSS, loadJS } from "../../helpers/loader.js";
import { settings_get, settings_put } from "../../lib/settings.js";
import assert from "../../lib/assert.js";
import ctrlError from "../ctrl_error.js";
import { ICON } from "./common_icon.js";
import { formatTimecode } from "./common_player.js";
import { transition, getDownloadUrl } from "./common.js";
import "../../components/menubar.js";
@@ -23,19 +25,22 @@ export default function(render) {
<div class="audioplayer_container">
<div class="audioplayer_box">
<div data-bind="loader" class="hidde">
<div class="audioplayer_loader" style="width: 31%;"></div>
<span class="percent">31%</span>
<div class="audioplayer_loader"></div>
<span class="percent"></span>
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0nMTIwcHgnIGhlaWdodD0nMTIwcHgnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIiBjbGFzcz0idWlsLXJpbmctYWx0Ij4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0ibm9uZSIgY2xhc3M9ImJrIj48L3JlY3Q+CiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNDAiIHN0cm9rZT0ibm9uZSIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48L2NpcmNsZT4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSIjNmY2ZjZmIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdHJva2UtZGFzaG9mZnNldCIgZHVyPSIycyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIGZyb209IjAiIHRvPSI1MDIiPjwvYW5pbWF0ZT4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9InN0cm9rZS1kYXNoYXJyYXkiIGR1cj0iMnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB2YWx1ZXM9IjE1MC42IDEwMC40OzEgMjUwOzE1MC42IDEwMC40Ij48L2FuaW1hdGU+CiAgPC9jaXJjbGU+Cjwvc3ZnPgo=" alt="loading">
</div>
<div id="waveform"></div>
<div class="audioplayer_control" style="opacity: 1;">
<div class="audioplayer_control hidden">
<div class="buttons no-select">
<span>
<img class="component_icon" draggable="false" src="${ICON.PLAY}" alt="play">
<img class="component_icon hidden" draggable="false" src="${ICON.PAUSE}" alt="pause">
<component-icon name="loading" class="hidden"></component-icon>
</span>
<span>
<img class="component_icon" draggable="false" src="${ICON.VOLUME_NORMAL}" alt="volume">
<img class="component_icon hidden" draggable="false" src="${ICON.VOLUME_MUTE}" alt="volume_mute">
<img class="component_icon hidden" draggable="false" src="${ICON.VOLUME_LOW}" alt="volume_low">
<img class="component_icon hidden" draggable="false" src="${ICON.VOLUME_NORMAL}" alt="volume">
</span>
<input type="range" min="0" max="100" value="52">
</div>
@@ -53,23 +58,75 @@ export default function(render) {
transition(qs($page, ".audioplayer_box"));
const $control = {
main: qs($page, `.audioplayer_control`),
play: qs($page, `.audioplayer_control [alt="play"]`),
pause: qs($page, `.audioplayer_control [alt="pause"]`),
// loading: qs($page, `.audioplayer_control component-icon[name="loading"]`),
loading: qs($page, `.audioplayer_control component-icon[name="loading"]`),
};
const $volume = {
range: qs($page, `input[type="range"]`),
// icon_mute: qs($page, `img[alt="volume_mute"]`),
// icon_low: qs($page, `img[alt="volume_low"]`),
// icon_normal: qs($page, `img[alt="volume"]`),
};
const setVolume = (volume, wavesurfer) => {
wavesurfer.setVolume(volume / 100);
icon_mute: qs($page, `img[alt="volume_mute"]`),
icon_low: qs($page, `img[alt="volume_low"]`),
icon_normal: qs($page, `img[alt="volume"]`),
};
const currentTime$ = new rxjs.BehaviorSubject([
0, // time of lastSeek: we want to start from the start
0, // seek offset to align with wavesurfer currentTime in the audiocontext
0, // starting time - does change when seeking to another point
0, // offset to align the audio context currentTime. otherwise when seeking, the
// currentTime keep growing and progress bar goes haywire
]);
const currentTime = (wavesurfer) => {
return currentTime$.value[0] + (wavesurfer.backend.ac.currentTime - currentTime$.value[1]);
};
const setVolume = (volume, wavesurfer) => {
settings_put("volume", volume);
wavesurfer.setVolume(volume / 100);
$volume.range.value = volume;
if (volume === 0) {
$volume.icon_mute.classList.remove("hidden");
$volume.icon_low.classList.add("hidden");
$volume.icon_normal.classList.add("hidden");
} else if (volume < 50) {
$volume.icon_mute.classList.add("hidden");
$volume.icon_low.classList.remove("hidden");
$volume.icon_normal.classList.add("hidden");
} else {
$volume.icon_mute.classList.add("hidden");
$volume.icon_low.classList.add("hidden");
$volume.icon_normal.classList.remove("hidden");
}
};
const setStatus = (status, wavesurfer) => {
switch (status) {
case "PLAYING":
$control.play.classList.add("hidden");
$control.pause.classList.remove("hidden");
$control.loading.classList.add("hidden");
wavesurfer.backend.ac.resume();
break;
case "PAUSED":
$control.play.classList.remove("hidden");
$control.pause.classList.add("hidden");
$control.loading.classList.add("hidden");
wavesurfer.backend.ac.suspend();
break;
case "BUFFERING":
$control.play.classList.add("hidden");
$control.pause.classList.add("hidden");
$control.loading.classList.remove("hidden");
break;
default:
assert.fail(status);
}
}
const setSeek = (newTime, wavesurfer) => {
currentTime$.next([newTime, wavesurfer.backend.ac.currentTime]);
wavesurfer.backend.source.stop(0);
wavesurfer.backend.disconnectSource();
wavesurfer.backend.createSource();
wavesurfer.backend.startPosition = newTime;
wavesurfer.backend.source.start(0, newTime);
};
// feature1: setup the dom
const setup$ = rxjs.of(qs($page, "#waveform")).pipe(
@@ -85,13 +142,16 @@ export default function(render) {
barWidth: 1,
}))),
rxjs.tap((wavesurfer) => {
window.audio = wavesurfer; // TODO: remove
wavesurfer.load(getDownloadUrl());
wavesurfer.on("error", (err) => {
wavesurfer.on("error", (err) => { // TODO: one liner to check
throw new Error(err)
});
wavesurfer.on("ready", () => {
$control.main.classList.remove("hidden");
qs($control.main, "#totalDuration").textContent = formatTimecode(wavesurfer.getDuration());
});
onDestroy(() => wavesurfer.destroy())
}),
rxjs.tap((wavesurfer) => onDestroy(() => wavesurfer.destroy())),
rxjs.catchError(ctrlError()),
rxjs.shareReplay(1),
);
@@ -115,7 +175,7 @@ export default function(render) {
}),
));
// feature3: prepare the data source
// feature3: connect the audio
const ready$ = setup$.pipe(
rxjs.mergeMap((wavesurfer) => new rxjs.Observable((observer) => {
wavesurfer.on("ready", () => observer.next(wavesurfer));
@@ -127,65 +187,121 @@ export default function(render) {
wavesurfer.backend.startPosition = 0;
wavesurfer.backend.lastPlay = 0;
wavesurfer.backend.source.start(0, 0);
wavesurfer.backend.ac.suspend();
})));
// feature: player control - volume
// feature4: hint of song progress
effect(setup$.pipe(
rxjs.mergeMap(() => rxjs.merge(
onClick($control.play).pipe(rxjs.mapTo(STATUS_PLAYING)),
rxjs.fromEvent(document, "keydown").pipe(rxjs.mapTo(STATUS_PLAYING)),
)),
rxjs.first(),
rxjs.mergeMap((status) => setup$.pipe(rxjs.tap((wavesurfer) => setStatus(status, wavesurfer)))),
rxjs.switchMap((wavesurfer) => rxjs.animationFrames().pipe(
rxjs.tap(() => {
const _currentTime = currentTime(wavesurfer)
const percent = _currentTime / wavesurfer.getDuration();
if (percent > 1) return;
wavesurfer.drawer.progress(percent);
qs($control.main, "#currentTime").textContent = formatTimecode(_currentTime);
}),
)),
));
// feature5: player control - play / pause
effect(setup$.pipe(
rxjs.mergeMap((wavesurfer) => rxjs.merge(
onClick($control.play).pipe(rxjs.mapTo(STATUS_PLAYING)),
onClick($control.pause).pipe(rxjs.mapTo(STATUS_PAUSED)),
).pipe(
rxjs.tap((status) => setStatus(status, wavesurfer)),
)),
));
// feature6: player control - volume
effect(setup$.pipe(
rxjs.switchMap(() => rxjs.fromEvent($volume.range, "input").pipe(rxjs.map((e) => e.target.value))),
rxjs.startWith(settings_get("volume") === null ? 80 : settings_get("volume")),
rxjs.mergeMap((volume) => setup$.pipe(rxjs.tap((wavesurfer) => setVolume(volume, wavesurfer)))),
rxjs.mergeMap((volume) => setup$.pipe(rxjs.tap((wavesurfer) => setVolume(parseInt(volume), wavesurfer)))),
));
// feature: player control - seek
effect(setup$.pipe(
// feature7: player control - seek
effect(ready$.pipe(
rxjs.mergeMap((wavesurfer) => rxjs.fromEvent(qs($page, "#waveform"), "click").pipe(
rxjs.map((e) => ({ e, wavesurfer })),
)),
rxjs.map(({ e, wavesurfer }) => {
const rec = e.target.getBoundingClientRect();
const rec = e.target.closest("#waveform").getBoundingClientRect();
return { wavesurfer, progress: (e.clientX - rec.x) / rec.width };
}),
rxjs.tap(({ progress, wavesurfer }) => {
wavesurfer.drawer.progress(progress);
const newTime = wavesurfer.getDuration() * progress;
currentTime$.next([newTime, wavesurfer.backend.ac.currentTime]);
wavesurfer.backend.source.stop(0);
wavesurfer.backend.disconnectSource();
wavesurfer.backend.createSource();
wavesurfer.backend.startPosition = newTime;
wavesurfer.backend.source.start(0, newTime);
setSeek(newTime, wavesurfer);
}),
));
// feature4: time progression
// feature8: player control - keyboard shortcut
effect(ready$.pipe(
rxjs.mergeMap(() => rxjs.merge(
onClick($control.play).pipe(rxjs.mapTo(STATUS_PLAYING)),
onClick($control.pause).pipe(rxjs.mapTo(STATUS_PAUSED)),
)),
rxjs.mergeMap((status) => setup$.pipe(rxjs.tap((wavesurfer) => {
switch (status) {
case STATUS_PLAYING:
wavesurfer.backend.ac.resume();
$control.play.classList.add("hidden");
$control.pause.classList.remove("hidden");
break;
case STATUS_PAUSED:
wavesurfer.backend.ac.suspend();
$control.play.classList.remove("hidden");
$control.pause.classList.add("hidden");
break;
default:
throw new Error("Unknown state");
}
}))),
rxjs.switchMap((wavesurfer) => rxjs.animationFrames().pipe(
rxjs.tap(() => {
const percent = (currentTime$.value[0] + (wavesurfer.backend.ac.currentTime - currentTime$.value[1])) / wavesurfer.getDuration();
if (percent > 1) return;
wavesurfer.drawer.progress(percent);
rxjs.switchMap((wavesurfer) => rxjs.fromEvent(document, "keydown").pipe(
rxjs.map((e) => e.code),
rxjs.tap((code) => {
switch(code) {
case "Space":
case "KeyK":
setStatus(
wavesurfer.backend.ac.state === "suspended" ?
STATUS_PLAYING : STATUS_PAUSED,
wavesurfer,
);
break;
case "KeyM":
setVolume(wavesurfer.getVolume() > 0 ? 0 : settings_get("volume"), wavesurfer);
break;
case "ArrowUp":
setVolume(Math.min(wavesurfer.getVolume()*100 + 10, 100), wavesurfer);
break;
case "ArrowDown":
setVolume(Math.max(wavesurfer.getVolume()*100 - 10, 0), wavesurfer);
break;
case "KeyL":
setSeek(Math.min(wavesurfer.getDuration(), currentTime(wavesurfer) + 10), wavesurfer);
break;
case "KeyJ":
setSeek(Math.max(0, currentTime(wavesurfer) - 10), wavesurfer);
break;
case "Digit0":
setSeek(0, wavesurfer);
break;
case "Digit1":
setSeek(wavesurfer.getDuration() / 10, wavesurfer);
break;
case "Digit2":
setSeek(2 * wavesurfer.getDuration() / 10, wavesurfer);
break;
case "Digit3":
setSeek(3 * wavesurfer.getDuration() / 10, wavesurfer);
break;
case "Digit4":
setSeek(4 * wavesurfer.getDuration() / 10, wavesurfer);
break;
case "Digit5":
setSeek(5 * wavesurfer.getDuration() / 10, wavesurfer);
break;
case "Digit6":
setSeek(6 * wavesurfer.getDuration() / 10, wavesurfer);
break;
case "Digit7":
setSeek(7 * wavesurfer.getDuration() / 10, wavesurfer);
break;
case "Digit8":
setSeek(8 * wavesurfer.getDuration() / 10, wavesurfer);
break;
case "Digit9":
setSeek(9 * wavesurfer.getDuration() / 10, wavesurfer);
break;
}
}),
)),
));
@@ -28,7 +28,8 @@ export default function(render) {
replacements: "blobUrl",
});
const rendition = book.renderTo($epub, {
height: "100%", width: "100%",
height: "100%",
width: "100%",
flow: "scrolled-doc",
method: "continuous",
allowScriptedContent: false,
@@ -17,11 +17,13 @@ export default async function(render) {
const $page = createElement(`
<div class="component_ide">
<component-menubar class="hidden"></component-menubar>
<div class="component_editor"></div>
<div class="component_editor hidden"></div>
</div>
`);
render($page);
const $editor = qs($page, ".component_editor");
const $menubar = qs($page, "component-menubar");
const content$ = ajax(getDownloadUrl()).pipe(
rxjs.map(({ response }) => response),
rxjs.shareReplay(),
@@ -33,7 +35,7 @@ export default async function(render) {
ajax("/about").pipe(rxjs.delay(TIME_BEFORE_ABORT_EDIT), rxjs.map(() => null)),
).pipe(
rxjs.mergeMap((content) => {
if (content === null || has_binary(content)) {
// if (content === null || has_binary(content)) {
return rxjs.from(initDownloader()).pipe(
removeLoader,
rxjs.mergeMap(() => {
@@ -41,12 +43,13 @@ export default async function(render) {
return rxjs.EMPTY;
}),
);
}
// }
return rxjs.of(content);
}),
removeLoader,
rxjs.tap((content) => {
window.CodeMirror(qs($page, ".component_editor"), {
$editor.classList.remove("hidden");
window.CodeMirror($editor, {
value: content,
lineNumbers: true,
// mode: mode,
@@ -62,8 +65,8 @@ export default async function(render) {
matchTags: { bothTags: true },
autoCloseTags: true,
});
transition(qs($page, ".component_editor"));
qs($page, "component-menubar").classList.remove("hidden");
transition($editor);
$menubar.classList.remove("hidden");
}),
rxjs.catchError(ctrlError()),
))
@@ -18,21 +18,21 @@ export default function(render) {
const $page = createElement(`
<div class="component_formviewer">
<component-menubar></component-menubar>
<div class="formviewer_container">
<div class="formviewer_container hidden">
<form class="sticky box"></form>
</div>
<button is="component-fab"></button>
</div>
`);
render($page);
transition(qs($page, ".formviewer_container"));
const file$ = new rxjs.ReplaySubject(1);
const $container = qs($page, ".formviewer_container");
const $fab = qs($page, `[is="component-fab"]`);
const formState = () => [...new FormData(qs($page, "form"))].reduce((acc, el) => {
acc[el[0]] = el[1];
return acc;
}, {});
const file$ = new rxjs.ReplaySubject(1);
// feature1: setup the dom
effect(getFile$().pipe(
@@ -55,6 +55,10 @@ export default function(render) {
`),
}))).pipe(
applyMutation(qs($page, "form"), "replaceChildren"),
rxjs.tap(() => {
$container.classList.remove("hidden");
transition($container);
}),
rxjs.mapTo(formSpec),
)),
rxjs.tap((formSpec) => file$.next(formObjToJSON(formSpec))),
@@ -6,7 +6,6 @@ body:not(.dark-mode) .component_appframe {
text-align: center;
width: 100%;
}
.component_appframe iframe {
width: 100%;
height: 100%;
@@ -1,17 +1,42 @@
import { createElement } from "../../lib/skeleton/index.js";
import rxjs, { effect } from "../../lib/rx.js";
import { loadCSS } from "../../helpers/loader.js";
import assert from "../../lib/assert.js";
import ctrlError from "../ctrl_error.js";
export default function(render, opts = {}) {
const { endpoint = null } = opts;
assert.truthy(endpoint);
const $page = createElement(`
<div class="component_appframe">
<component-menubar></component-menubar>
<div>
IFRAME
</div>
<iframe src="${getFrameUrl(endpoint)}"></iframe>
</div>
`);
render($page);
effect(rxjs.fromEvent(window, "message").pipe(
rxjs.filter((event) => event.origin === location.origin),
rxjs.tap((event) => { // TODO: notification
switch(event.data.type) {
case "notify::info":
notify.send(t(event.data.message), "info");
break;
case "notify::error":
notify.send(t(event.data.message), "error");
break
}
}),
));
effect(rxjs.of(assert.truthy(endpoint)).pipe(
rxjs.catchError(ctrlError()),
));
}
export function init() {
return loadCSS(import.meta.url, "./application_iframe.css");
}
function getFrameUrl(endpoint) {
return endpoint + "?path=/home/mickael/Downloads/office.docx" + "&share=";
}
@@ -1,8 +1,9 @@
body:not(.dark-mode) .component_imageviewer .component_image_container, body:not(.dark-mode) .component_imageviewer .component_image_container .fullscreen .component_pager .wrapper > span {
.component_imageviewer .component_image_container,
body:not(.dark-mode) .component_imageviewer .component_image_container .fullscreen .component_pager .wrapper > span {
background: #525659;
}
.component_imageviewer, .component_imageviewer .component_image_container > .images_wrapper, .component_imageviewer .component_image_container > .images_wrapper > span, .component_imageviewer .component_image_container > .images_wrapper > span > div {
.component_imageviewer, .component_imageviewer .component_image_container > .images_wrapper {
flex: 1;
display: flex;
overflow: hidden;
@@ -27,7 +28,7 @@ body:not(.dark-mode) .component_imageviewer .component_image_container, body:not
.component_imageviewer .component_image_container.fullscreen .component_pager .wrapper > span {
background: var(--dark);
}
.component_imageviewer .component_image_container.fullscreen img[srcset] {
.component_imageviewer .component_image_container.fullscreen img.photo {
background: var(--color);
}
@media screen and (max-height: 410px) {
@@ -162,7 +163,7 @@ body:not(.dark-mode) .component_imageviewer .component_image_container, body:not
.component_imageviewer .component_image_container .images_aside .content .more_container .meta_key .value {
color: var(--bg-color);
}
.component_imageviewer .component_image_container img[srcset] {
.component_imageviewer .component_image_container img.photo:not(.error) {
margin: auto;
max-height: 100%;
max-width: 100%;
@@ -171,7 +172,7 @@ body:not(.dark-mode) .component_imageviewer .component_image_container, body:not
box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px;
border-radius: 2px;
}
.component_imageviewer .component_image_container img[srcset].idle {
.component_imageviewer .component_image_container img.photo.idle {
transition: 0.2s ease transform;
}
@@ -196,7 +197,6 @@ body:not(.dark-mode) .component_imageviewer .component_image_container, body:not
margin-top: 10px;
}
.component_imageviewer .component_image_container img.error {
filter: contrast(0.8);
width: 160px;
@@ -11,6 +11,7 @@ import Hls from "../../lib/vendor/hlsjs/hls.js";
import ctrlError from "../ctrl_error.js";
import { transition, getDownloadUrl } from "./common.js";
import { formatTimecode } from "./common_player.js";
import { ICON } from "./common_icon.js";
import { menubarDownload, buildMenubar } from "./common_menubar.js";
import { render as renderMenubar } from "../../components/menubar.js";
@@ -367,9 +368,3 @@ export function init() {
if (typeof window.overrides["video-map-sources"] !== "function") window.overrides["video-map-sources"] = (s) => (s);
});
}
function formatTimecode(seconds) {
return String(parseInt(seconds / 60)).padStart(2, "0") +
":"+
String(parseInt(seconds % 60)).padStart(2, "0");
}
@@ -0,0 +1,7 @@
export function formatTimecode(seconds) {
return String(parseInt(seconds / 60)).padStart(2, "0") +
":"+
String(parseInt(seconds % 60)).padStart(2, "0");
}
// TODO: abstract setVolume, setSeek and setStatus