aboutsummaryrefslogtreecommitdiffstats
path: root/src/media-timeline.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/media-timeline.js')
-rw-r--r--src/media-timeline.js42
1 files changed, 21 insertions, 21 deletions
diff --git a/src/media-timeline.js b/src/media-timeline.js
index bc9f4c7..d8ae008 100644
--- a/src/media-timeline.js
+++ b/src/media-timeline.js
@@ -76,28 +76,26 @@ export function loadWaveform(url) {
return job;
}
+// Where a moment sits along a bar: a percentage of the clip, or null.
+export function markAt(seconds, duration) {
+ return Number.isFinite(duration) && duration > 0 ? Math.min(100, Math.max(0, seconds / duration * 100)) : null;
+}
+
// The live player owns this subtree, so morphing cannot replace its handlers
-// with closures pointing at a detached player.
-export function mediaTimeline(player, url, { h, event, moments, action, label }) {
+// with closures pointing at a detached player. What was said, reacted or
+// zapped at a moment sits on the bar at that moment; replies, reactions and
+// zaps made while a clip plays carry its time on their own (see momentOf).
+export function mediaTimeline(player, url, { h, event, moments, glyph, label, onMarks = null }) {
const audio = player.tagName === 'AUDIO';
const box = h('div', { class: 'media-timeline', onClick: e => e.stopPropagation(), onPointerdown: e => e.stopPropagation() });
box._skipMorph = true;
const bars = h('div', { class: 'media-wave', 'aria-hidden': 'true' });
const range = h('input', { type: 'range', min: 0, max: 1000, value: 0, step: 1, 'aria-label': label('mediaSeek'), disabled: true });
- const seek = h('div', { class: 'media-seek' }, bars, range);
- const time = h('span', { class: 'small muted' }, '0:00');
const marks = h('div', { class: 'media-marks' });
+ const seek = h('div', { class: 'media-seek' }, bars, range, marks);
+ const time = h('span', { class: 'small muted media-time' }, '0:00');
const detail = h('div', { class: 'small media-moment-detail', 'aria-live': 'polite' });
- const actions = h('div', { class: 'row media-moment-actions' }, time);
- for (const [kind, icon] of [['comment', '↩'], ['reaction', '♡'], ['zap', '⚡']]) {
- actions.append(h('button', { type: 'button', class: 'btn-sm', 'aria-label': label(kind === 'comment' ? 'msgReply' : kind === 'reaction' ? 'postLike' : 'zapTitle') + ' ' + label('mediaAtTime'), onClick: e => {
- e.stopPropagation();
- const ev = event();
- const m = mediaMoment(url, player.currentTime);
- if (ev && m) action(kind, ev, m, e.currentTarget);
- } }, icon + ' ' + label('mediaAtTime')));
- }
- box.append(seek, actions, marks, detail);
+ box.append(seek, time, detail);
let waveStarted = false, signature = '';
const draw = values => {
if (!values) return;
@@ -112,29 +110,31 @@ export function mediaTimeline(player, url, { h, event, moments, action, label })
const update = () => {
const ev = event();
const finite = Number.isFinite(player.duration) && player.duration > 0;
- actions.hidden = !ev || !finite;
range.disabled = !finite;
range.value = finite ? player.currentTime / player.duration * 1000 : 0;
range.setAttribute('aria-valuetext', mediaClock(player.currentTime) + ' / ' + mediaClock(finite ? player.duration : 0));
seek.style.setProperty('--played', (finite ? player.currentTime / player.duration * 100 : 0) + '%');
time.textContent = mediaClock(player.currentTime) + ' / ' + mediaClock(finite ? player.duration : 0);
- for (const b of actions.querySelectorAll('button')) b.title = b.textContent + ' ' + mediaClock(player.currentTime);
if (audio && !waveStarted && finite) {
waveStarted = true;
const published = waveformFromTags(ev, url);
if (published) draw(published);
else if (player.duration <= 1200 && !navigator.connection?.saveData) loadWaveform(url).then(draw);
}
- const items = ev ? moments(ev.id, url) : [];
- const next = items.map(x => x.id).join(',');
+ const items = ev && finite ? moments(ev.id, url) : [];
+ const next = items.map(x => x.id).join(',') + '@' + (finite ? player.duration : 0);
+ // a video has its own seek bar: this one shows under it only to carry
+ // what was said at a moment (two bars read as a glitch on a phone)
+ box.classList.toggle('has-marks', items.length > 0);
+ if (onMarks) onMarks(items.length > 0);
if (signature !== next) {
signature = next;
- marks.replaceChildren(...items.map(x => h('button', { type: 'button', class: 'media-mark btn-sm',
- title: x.text, onClick: () => { jump(x.moment.seconds); detail.textContent = x.text || x.icon; } }, mediaClock(x.moment.seconds) + ' ' + x.icon)));
+ marks.replaceChildren(...items.map(x => h('button', { type: 'button', class: 'media-mark', title: mediaClock(x.moment.seconds) + ' ' + x.text,
+ style: 'left:' + markAt(x.moment.seconds, player.duration) + '%',
+ onClick: e => { e.stopPropagation(); jump(x.moment.seconds); detail.textContent = mediaClock(x.moment.seconds) + ' ' + x.text; } }, glyph(x))));
}
};
// Fetching/decoding starts only once the user plays (or explicitly seeks).
for (const type of ['timeupdate', 'playing', 'seeked', 'durationchange', 'mediaactivity']) player.addEventListener(type, update);
- actions.hidden = true;
return box;
}