Improved timeline thumbnail

This commit is contained in:
WardPearce
2026-02-11 04:29:53 +13:00
parent 7c3ecde6b0
commit fd3dd21364
6 changed files with 62 additions and 49 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.13.16",
"version": "1.13.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.13.16",
"version": "1.13.17",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
-1
View File
@@ -190,7 +190,6 @@ export class ElectronCapacitorApp {
globalShortcut.register('Control+Shift+I', () => {
this.MainWindow.webContents.toggleDevTools();
});
// Security
this.MainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
+55 -41
View File
@@ -128,20 +128,27 @@
let playerTimelineTooltip: HTMLDivElement | undefined = $state();
let playerTimelineThumbnails: TimelineThumbnail[] = $state([]);
let playerTimelineThumbnailsCache = new ImageCache();
let playerTimelineThumbnailCanvas: HTMLCanvasElement | undefined = $state();
let playerTimelineThumbnailCanvas: {
timeline?: HTMLCanvasElement;
thumb?: HTMLCanvasElement;
} = $state({});
let playerInitalInteract = true;
const playerTimelineSlider = new Slider({
min: 0,
step: 0.1,
value: () => currentTime,
onValueChange: (timeToSet) => {
onValueChange: async (timeToSet) => {
userManualSeeking = true;
currentTime = timeToSet;
showPlayerUI();
setPlayerTimelineChapters(currentTime);
if (playerTimelineThumbnailCanvas.thumb) {
await setPlayerTimelineThumbnails(currentTime, playerTimelineThumbnailCanvas.thumb);
}
if (playerSliderDebounce) clearTimeout(playerSliderDebounce);
playerSliderDebounce = setTimeout(() => {
@@ -515,13 +522,14 @@
dashUrl += '?local=true';
}
if (data.video.storyboards && data.video.storyboards.length > 1) {
// Use best possible thumbnails
playerTimelineThumbnails = await storyboardThumbnails(
data.video.storyboards[data.video.storyboards.length - 1],
if (data.video.storyboards && data.video.storyboards.length > 2) {
storyboardThumbnails(
data.video.storyboards[2],
playerTimelineThumbnailsCache,
playerMaxKnownTime
);
).then((thumbnails) => {
playerTimelineThumbnails = thumbnails;
});
}
if (
@@ -1127,6 +1135,19 @@
}
}
async function setPlayerTimelineThumbnails(time: number, canvas: HTMLCanvasElement) {
const canvasContext = canvas.getContext('2d');
if (canvasContext) {
await drawTimelineThumbnail(
canvasContext,
playerTimelineThumbnailsCache,
playerTimelineThumbnails,
time
);
}
}
async function updateTooltip() {
if (!playerSliderElement || latestMouseX === undefined) {
requestAnimationTooltip = undefined;
@@ -1139,17 +1160,11 @@
playerTimelineTimeHover = percent * (data.video.lengthSeconds ?? 0);
setPlayerTimelineChapters(playerTimelineTimeHover);
if (playerTimelineThumbnailCanvas) {
const canvasContext = playerTimelineThumbnailCanvas.getContext('2d');
if (canvasContext) {
await drawTimelineThumbnail(
canvasContext,
playerTimelineThumbnailsCache,
playerTimelineThumbnails,
playerTimelineTimeHover
);
}
if (playerTimelineThumbnailCanvas.timeline) {
await setPlayerTimelineThumbnails(
playerTimelineTimeHover,
playerTimelineThumbnailCanvas.timeline
);
}
if (playerTimelineTooltip) {
@@ -1350,35 +1365,31 @@
onmousemove={timelineMouseMove}
bind:this={playerSliderElement}
>
{#snippet timelineTooltip()}
{#snippet timelineTooltip(key: 'thumb' | 'timeline')}
{#if playerTimelineThumbnails.length > 0}
<canvas
bind:this={playerTimelineThumbnailCanvas[key]}
width={playerTimelineThumbnails[0].width}
height={playerTimelineThumbnails[0].height}
></canvas>
{/if}
{#if playerCloestSponsor}
{sponsorSegments[playerCloestSponsor.category]}
<br />
<p class="no-margin">{sponsorSegments[playerCloestSponsor.category]}</p>
{:else if playerCloestTimestamp}
{truncate(playerCloestTimestamp.title, 22)}
<br />
<p class="no-margin">{truncate(playerCloestTimestamp.title, 22)}</p>
{/if}
{/snippet}
<div class="track">
{#if !userManualSeeking}
<div bind:this={playerTimelineTooltip} class="timeline tooltip">
{#if playerTimelineThumbnails.length > 0}
<canvas
bind:this={playerTimelineThumbnailCanvas}
width={playerTimelineThumbnails[0].width}
height={playerTimelineThumbnails[0].height}
>
></canvas
>
{/if}
{@render timelineTooltip()}
{@render timelineTooltip('timeline')}
{videoLength(playerTimelineTimeHover)}
</div>
{/if}
<div class="range"></div>
<div {...playerTimelineSlider.thumb}>
<div class="tooltip thumb-tooltip">
{@render timelineTooltip()}
<div class="tooltip thumb">
{@render timelineTooltip('thumb')}
{videoLength(currentTime)}
</div>
</div>
@@ -1748,10 +1759,6 @@
transform: translate(-50%, -50%);
}
.thumb-tooltip {
left: var(--percentage);
}
.seek-double-click {
background-color: var(--secondary-container);
height: var(--video-player-height);
@@ -1873,13 +1880,20 @@
pointer-events: none;
will-change: transform;
padding: 0;
border-radius: 0.25rem;
display: block;
}
.timeline.tooltip canvas {
.timeline.tooltip canvas,
.tooltip.thumb canvas {
display: block;
margin-bottom: 0.1rem;
height: 100px;
border-radius: 0.25rem;
}
.tooltip.thumb {
left: var(--percentage);
display: block;
padding: 0;
}
</style>
+3 -3
View File
@@ -40,9 +40,6 @@ export async function storyboardThumbnails(
for (let sheetIndex = 0; sheetIndex < storyboardCount; sheetIndex++) {
const url = templateUrl.replace('M$M', `M${sheetIndex}`);
// Preload all images.
imageCache.load(url);
for (let i = 0; i < count; i++) {
const localIndex = i % (cols * rows);
const row = Math.floor(localIndex / cols);
@@ -51,6 +48,9 @@ export async function storyboardThumbnails(
const time = Math.floor((globalIndex / totalThumbnails) * videoDurationMs);
// Pre-cache sheet
imageCache.load(url);
thumbnails.push({
url,
time,
+1 -1
View File
@@ -352,7 +352,7 @@
{#if isLoggedIn}
<button class="circle large transparent" onclick={() => ui('#dialog-notifications')}
><i>notifications</i>
<div class="badge">
<div class="badge secondary">
{#if notifications.length > 99}
99+
{:else}
File diff suppressed because one or more lines are too long