Merge pull request #1394 from Materialious/update/1.13.18
Update/1.13.18
This commit is contained in:
@@ -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">
|
||||
|
||||
Generated
+2
-2
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.13.17",
|
||||
"version": "1.13.18",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.13.17",
|
||||
"version": "1.13.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run patch:github && vite dev",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user