diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index f402c2b1..86da4d5a 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -2,7 +2,7 @@ import { page } from '$app/stores'; import { getBestThumbnail } from '$lib/images'; import { padTime, videoLength } from '$lib/numbers'; - import { type PhasedDescription } from '$lib/timestamps'; + import { type PhasedDescription, type Timestamp } from '$lib/timestamps'; import { SafeArea, SystemBarsStyle, SystemBarsType } from '@capacitor-community/safe-area'; import { Capacitor } from '@capacitor/core'; import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation'; @@ -95,9 +95,9 @@ let playerTimelineTooltipVisible: boolean = $state(false); let playerTimelineTimeHover: number = $state(0); let playerTimelineMouseX: number = $state(0); - let playerTimelineLastUpdate: number = 0; let playerVideoEndTimePretty: string = $state(''); let playerBufferedTo: number = $state(0); + let playerCloestTimestamp: Timestamp | undefined = $state(); let clickCount = $state(0); // eslint-disable-next-line no-undef @@ -112,6 +112,10 @@ updateVideoPlayerHeight(); }); + function getMarkerWidth(startTime: number, endTime: number): string { + return `${((endTime - startTime) / playerMaxKnownTime) * 100}%`; + } + function restoreDefaultLanguage() { if (!$playerDefaultLanguage || $playerDefaultLanguage === 'original') { const languageAndRole = player @@ -441,8 +445,7 @@ if (data.content.timestamps) { let chapterWebVTT = 'WEBVTT\n\n'; - let timestampIndex = 0; - data.content.timestamps.forEach((timestamp) => { + data.content.timestamps.forEach((timestamp, timestampIndex) => { let endTime: string; if (timestampIndex === data.content.timestamps.length - 1) { endTime = videoLength(data.video.lengthSeconds); @@ -451,15 +454,15 @@ } chapterWebVTT += `${padTime(timestamp.timePretty)}.000 --> ${padTime(endTime)}.000\n${timestamp.title.replaceAll('-', '').trim()}\n\n`; - - timestampIndex += 1; }); - if (timestampIndex > 0) { + try { player.addChaptersTrack( `data:text/vtt;base64,${btoa(chapterWebVTT)}`, get(playerDefaultLanguage) ); + } catch { + // Continue regardless } } } else { @@ -918,24 +921,33 @@ } } + let requestAnimationTooltip: number | undefined; function handleMouseMove(event: MouseEvent) { - playerPauseTimeUpdates = true; + if (requestAnimationTooltip) return; - const currentTime = Date.now(); - if (currentTime - playerTimelineLastUpdate < 60) return; - playerTimelineLastUpdate = currentTime; + requestAnimationTooltip = requestAnimationFrame(() => { + const input = event.target as HTMLInputElement; + const rect = input.getBoundingClientRect(); + playerTimelineMouseX = event.clientX - rect.left; - const input = event.target as HTMLInputElement; - const boundingRect = input.getBoundingClientRect(); - playerTimelineMouseX = event.clientX - boundingRect.left; + const percent = Math.min(Math.max(playerTimelineMouseX / input.clientWidth, 0), 1); + playerTimelineTimeHover = percent * (data.video.lengthSeconds ?? 0); - const percent = playerTimelineMouseX / input.clientWidth; - playerTimelineTimeHover = Math.round(percent * (data.video.lengthSeconds || 0)); - playerTimelineTooltipVisible = true; + playerCloestTimestamp = data.content.timestamps.find((chapter, chapterIndex) => { + let endTime: number; + if (chapterIndex === data.content.timestamps.length - 1) { + endTime = data.video.lengthSeconds; + } else { + endTime = data.content.timestamps[chapterIndex + 1].time; + } + return playerTimelineTimeHover >= chapter.time && playerTimelineTimeHover < endTime; + }); + playerTimelineTooltipVisible = true; + requestAnimationTooltip = undefined; + }); } function handleMouseLeave() { - playerPauseTimeUpdates = false; playerTimelineTooltipVisible = false; } @@ -984,7 +996,7 @@ } clickCount = 0; - }, 300); + }, 200); if (clickCount < 2) return; @@ -1116,10 +1128,10 @@
+ {#each data.content.timestamps as chapter, index (chapter)} +
+ {/each} {#if playerTimelineTooltipVisible}
+ {#if playerCloestTimestamp} + {playerCloestTimestamp.title} + {/if} +
+ {videoLength(playerTimelineTimeHover)}
{/if} @@ -1160,7 +1187,8 @@ step="0.1" max="1" /> - {/key} + {/key} + {/if} @@ -1503,6 +1531,19 @@ border-bottom-right-radius: 2rem; } + .chapter-marker { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 0; + height: 1rem; + background-color: var(--secondary); + opacity: 0.5; + border-radius: 2rem; + z-index: 0; + pointer-events: none; + } + menu.mobile { transform: scale(1) translateY(-40%) translateX(0); width: 300px; diff --git a/materialious/src/lib/timestamps.ts b/materialious/src/lib/timestamps.ts index dc28a1c7..f61b7c93 100644 --- a/materialious/src/lib/timestamps.ts +++ b/materialious/src/lib/timestamps.ts @@ -1,7 +1,8 @@ import { decodeHtmlCharCodes } from './misc'; import { convertToSeconds } from './numbers'; -export type Timestamps = { title: string; time: number; timePretty: string; endTime: number }[]; +export type Timestamp = { title: string; time: number; timePretty: string; endTime: number }; +export type Timestamps = Timestamp[]; export interface PhasedDescription { description: string;