Merge pull request #1394 from Materialious/update/1.13.18

Update/1.13.18
This commit is contained in:
Ward
2026-02-11 07:22:42 +13:00
committed by GitHub
9 changed files with 115 additions and 85 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 201
versionName "1.13.17"
versionCode 202
versionName "1.13.18"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -100,7 +100,11 @@
<release version="1.13.17" date="2026-2-11">
<release version="1.13.18" date="2026-2-11">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.18</url>
</release>
<release version="1.13.17" date="2026-2-11">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.17</url>
</release>
<release version="1.13.16" date="2026-2-10">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.13.17",
"version": "1.13.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.13.17",
"version": "1.13.18",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.13.17",
"version": "1.13.18",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.13.17",
"version": "1.13.18",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
+23 -13
View File
@@ -133,12 +133,14 @@
thumb?: HTMLCanvasElement;
} = $state({});
let playerInitalInteract = true;
let playerSliderInteracted = $state(false);
const playerTimelineSlider = new Slider({
min: 0,
step: 0.1,
value: () => currentTime,
onValueChange: async (timeToSet) => {
playerSliderInteracted = true;
userManualSeeking = true;
currentTime = timeToSet;
@@ -155,6 +157,7 @@
if (playerElement) {
playerElement.currentTime = timeToSet;
userManualSeeking = false;
playerSliderInteracted = false;
}
}, 300);
},
@@ -523,13 +526,13 @@
}
if (data.video.storyboards && data.video.storyboards.length > 2) {
storyboardThumbnails(
data.video.storyboards[2],
playerTimelineThumbnailsCache,
playerMaxKnownTime
).then((thumbnails) => {
playerTimelineThumbnails = thumbnails;
});
try {
storyboardThumbnails(data.video).then((thumbnails) => {
playerTimelineThumbnails = thumbnails;
});
} catch {
// Continue regardless of error.
}
}
if (
@@ -1143,6 +1146,7 @@
canvasContext,
playerTimelineThumbnailsCache,
playerTimelineThumbnails,
data.video,
time
);
}
@@ -1374,9 +1378,13 @@
></canvas>
{/if}
{#if playerCloestSponsor}
<p class="no-margin">{sponsorSegments[playerCloestSponsor.category]}</p>
<p class="no-margin" style="padding: 0 0.5rem;">
{sponsorSegments[playerCloestSponsor.category]}
</p>
{:else if playerCloestTimestamp}
<p class="no-margin">{truncate(playerCloestTimestamp.title, 22)}</p>
<p class="no-margin" style="padding: 0 0.5rem;">
{truncate(playerCloestTimestamp.title, 22)}
</p>
{/if}
{/snippet}
<div class="track">
@@ -1388,10 +1396,12 @@
{/if}
<div class="range"></div>
<div {...playerTimelineSlider.thumb}>
<div class="tooltip thumb">
{@render timelineTooltip('thumb')}
{videoLength(currentTime)}
</div>
{#if playerSliderInteracted}
<div class="tooltip thumb">
{@render timelineTooltip('thumb')}
{videoLength(currentTime)}
</div>
{/if}
</div>
</div>
<div bind:this={playerBufferBar} class="buffered-bar" class:hide={userManualSeeking}></div>
+79 -63
View File
@@ -1,5 +1,8 @@
import type { StoryBoard } from './api/model';
import { get } from 'svelte/store';
import type { VideoPlay } from './api/model';
import { ImageCache } from './images';
import { instanceStore } from './store';
import { parseText } from 'media-captions';
export interface TimelineThumbnail {
url: string;
@@ -9,58 +12,70 @@ export interface TimelineThumbnail {
index: number;
}
export async function storyboardThumbnails(
storyboard: StoryBoard,
imageCache: ImageCache,
videoLength: number
): Promise<TimelineThumbnail[]> {
const { count, storyboardCount, width, height, templateUrl } = storyboard;
export async function storyboardThumbnails(video: VideoPlay): Promise<TimelineThumbnail[]> {
if (!video.storyboards || video.storyboards.length < 2) return [];
// Our ytjs implementation doesn't do this right,
// so just assume its incorrect and fetch manually.
// Doesn't actually add any extra overhead.
const firstSheetUrl = templateUrl.replace('M$M', `M0`);
const firstSheetImage = await imageCache.load(firstSheetUrl);
if (!firstSheetImage) {
return [];
}
const storyboardWidth = firstSheetImage.width;
const storyboardHeight = firstSheetImage.height;
const cols = Math.floor(storyboardWidth / width);
const rows = Math.floor(storyboardHeight / height);
const thumbnails: TimelineThumbnail[] = [];
const totalThumbnails = storyboardCount * count;
const videoDurationMs = videoLength * 1000;
if (video.fallbackPatch === 'youtubejs') {
const { count, storyboardCount, width, height, templateUrl } = video.storyboards[2];
let globalIndex = 0;
const totalThumbnails = storyboardCount * count;
const videoDurationMs = video.lengthSeconds * 1000;
const timeInterval = videoDurationMs / totalThumbnails;
for (let sheetIndex = 0; sheetIndex < storyboardCount; sheetIndex++) {
const url = templateUrl.replace('M$M', `M${sheetIndex}`);
// Pre-cache sheet
imageCache.load(url);
for (let i = 0; i < count; i++) {
const localIndex = i % (cols * rows);
const row = Math.floor(localIndex / cols);
if (row >= rows) break;
const time = Math.floor((globalIndex / totalThumbnails) * videoDurationMs);
for (let sheetIndex = 0; sheetIndex < storyboardCount; sheetIndex++) {
const url = templateUrl.replace('M$M', `M${sheetIndex}`);
const time = Math.floor(sheetIndex * count * timeInterval);
thumbnails.push({
url,
time,
width,
height,
index: globalIndex
index: sheetIndex * count
});
globalIndex++;
}
} else {
const WebVTTResp = await fetch(`${get(instanceStore)}${video.storyboards[2].url}`);
if (!WebVTTResp.ok) return [];
const thumbnailsSheets = await parseText(await WebVTTResp.text(), {
strict: true,
type: 'vtt'
});
const processedUrls = new Set<string>();
let index = 0;
thumbnailsSheets.cues.forEach((cue) => {
const urlParts = cue.text.split('#');
const cleanedUrl = urlParts[0];
if (!processedUrls.has(cleanedUrl)) {
const xywh = urlParts[1];
const xywhValues = xywh.split(',');
if (xywhValues.length !== 4) {
return [];
}
const [, , thumbWidth, thumbHeight] = xywhValues.map(Number);
processedUrls.add(cleanedUrl);
thumbnails.push({
url: cleanedUrl,
time: cue.startTime * 1000,
width: thumbWidth,
height: thumbHeight,
index
});
index++;
}
});
}
return thumbnails;
@@ -70,17 +85,19 @@ export async function drawTimelineThumbnail(
ctx: CanvasRenderingContext2D,
imageCache: ImageCache,
thumbnails: TimelineThumbnail[],
time: number
video: VideoPlay,
currentTime: number
): Promise<void> {
if (!thumbnails.length) return;
const timeInMs = time * 1000;
const timeInMs = currentTime * 1000;
const closest = thumbnails.reduce((prev, curr) =>
Math.abs(curr.time - timeInMs) < Math.abs(prev.time - timeInMs) ? curr : prev
);
const closest = thumbnails.reduce((prev, curr) => {
return Math.abs(curr.time - timeInMs) < Math.abs(prev.time - timeInMs) ? curr : prev;
});
if (!closest.url) return;
const nextThumbnail = thumbnails.find((thumb) => thumb.time > closest.time);
const spriteSheetEndTime = nextThumbnail ? nextThumbnail.time : video.lengthSeconds * 1000;
const img = await imageCache.load(closest.url);
if (!img) {
@@ -90,24 +107,23 @@ export async function drawTimelineThumbnail(
const sheetWidth = img.width;
const sheetHeight = img.height;
const cols = Math.floor(sheetWidth / closest.width);
const rows = Math.floor(sheetHeight / closest.height);
const thumbWidth = closest.width;
const thumbHeight = closest.height;
const cols = Math.floor(sheetWidth / thumbWidth);
const rows = Math.floor(sheetHeight / thumbHeight);
const thumbnailsPerSheet = cols * rows;
const indexInSheet = closest.index % thumbnailsPerSheet;
const thumbX = (indexInSheet % cols) * closest.width;
const thumbY = Math.floor(indexInSheet / cols) * closest.height;
const elapsedTime = timeInMs - closest.time;
const totalDuration = spriteSheetEndTime - closest.time;
const elapsedPercentage = Math.min(Math.max(elapsedTime / totalDuration, 0), 1);
ctx.clearRect(0, 0, closest.width, closest.height);
ctx.drawImage(
img,
thumbX,
thumbY,
closest.width,
closest.height,
0,
0,
closest.width,
closest.height
);
const indexInSheet = Math.floor(elapsedPercentage * thumbnailsPerSheet);
const thumbX = (indexInSheet % cols) * thumbWidth;
const thumbY = Math.floor(indexInSheet / cols) * thumbHeight;
ctx.clearRect(0, 0, thumbWidth, thumbHeight);
ctx.drawImage(img, thumbX, thumbY, thumbWidth, thumbHeight, 0, 0, thumbWidth, thumbHeight);
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.13.17"
LATEST_VERSION = "1.13.18"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")