Merge pull request #1389 from Materialious/update/1.13.16

Update/1.13.16
This commit is contained in:
Ward
2026-02-11 02:37:32 +13:00
committed by GitHub
12 changed files with 230 additions and 19 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 199
versionName "1.13.15"
versionCode 200
versionName "1.13.16"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -98,7 +98,11 @@
<release version="1.13.15" date="2026-2-10">
<release version="1.13.16" date="2026-2-10">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.16</url>
</release>
<release version="1.13.15" date="2026-2-10">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.15</url>
</release>
<release version="1.13.14" date="2026-2-09">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.13.15",
"version": "1.13.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.13.15",
"version": "1.13.16",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.13.15",
"version": "1.13.16",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
-1
View File
@@ -190,7 +190,6 @@ export class ElectronCapacitorApp {
globalShortcut.register('Control+Shift+I', () => {
this.MainWindow.webContents.toggleDevTools();
});
this.MainWindow.webContents.toggleDevTools();
// Security
this.MainWindow.webContents.setWindowOpenHandler((details) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.13.15",
"version": "1.13.16",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
+61 -7
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { getBestThumbnail } from '$lib/images';
import { getBestThumbnail, ImageCache } from '$lib/images';
import { padTime, videoLength } from '$lib/numbers';
import { type PhasedDescription, type Timestamp } from '$lib/timestamps';
import { SystemBars, SystemBarsStyle, SystemBarType } from '@capacitor/core';
@@ -58,7 +58,12 @@
import { Network, type ConnectionStatus } from '@capacitor/network';
import { fade } from 'svelte/transition';
import { addToast } from './Toast.svelte';
import { isMobile } from '$lib/misc';
import { isMobile, truncate } from '$lib/misc';
import {
drawTimelineThumbnail,
storyboardThumbnails,
type TimelineThumbnail
} from '$lib/timelineThumbnails';
interface Props {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
@@ -121,6 +126,9 @@
let playerVolumeElement: HTMLElement | undefined = $state();
let playerIsFullscreen: boolean = $state(false);
let playerTimelineTooltip: HTMLDivElement | undefined = $state();
let playerTimelineThumbnails: TimelineThumbnail[] = $state([]);
let playerTimelineThumbnailsCache = new ImageCache();
let playerTimelineThumbnailCanvas: HTMLCanvasElement | undefined = $state();
let playerInitalInteract = true;
const playerTimelineSlider = new Slider({
@@ -507,6 +515,15 @@
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],
playerTimelineThumbnailsCache,
playerMaxKnownTime
);
}
if (
Capacitor.getPlatform() === 'android' &&
$playerProxyVideosStore &&
@@ -1110,7 +1127,7 @@
}
}
function updateTooltip() {
async function updateTooltip() {
if (!playerSliderElement || latestMouseX === undefined) {
requestAnimationTooltip = undefined;
return;
@@ -1122,6 +1139,19 @@
playerTimelineTimeHover = percent * (data.video.lengthSeconds ?? 0);
setPlayerTimelineChapters(playerTimelineTimeHover);
if (playerTimelineThumbnailCanvas) {
const canvasContext = playerTimelineThumbnailCanvas.getContext('2d');
if (canvasContext) {
await drawTimelineThumbnail(
canvasContext,
playerTimelineThumbnailsCache,
playerTimelineThumbnails,
playerTimelineTimeHover
);
}
}
if (playerTimelineTooltip) {
const tooltipWidth = playerTimelineTooltip.offsetWidth;
const tooltipHeight = playerTimelineTooltip.offsetHeight;
@@ -1218,6 +1248,8 @@
// Continue regardless of error
}
playerTimelineThumbnailsCache.clear();
window.removeEventListener('resize', updateVideoPlayerHeight);
Mousetrap.unbind(['left', 'right', 'space', 'c', 'f', 'shift+left', 'shift+right', 'enter']);
@@ -1323,13 +1355,22 @@
{sponsorSegments[playerCloestSponsor.category]}
<br />
{:else if playerCloestTimestamp}
{playerCloestTimestamp.title}
{truncate(playerCloestTimestamp.title, 22)}
<br />
{/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()}
{videoLength(playerTimelineTimeHover)}
</div>
@@ -1614,6 +1655,10 @@
{/if}
<style>
:root {
--player-timeline-height: 1.1rem;
}
#player-container {
position: relative;
width: 100%;
@@ -1661,7 +1706,7 @@
}
.player-slider {
height: 1rem;
height: var(--player-timeline-height);
margin: 0 auto;
border-radius: 0.25rem;
}
@@ -1732,7 +1777,7 @@
.buffered-bar {
position: absolute;
height: 1rem;
height: var(--player-timeline-height);
background: var(--secondary);
top: 50%;
left: 0;
@@ -1749,7 +1794,7 @@
top: 50%;
transform: translateY(-50%);
left: 0;
height: 1rem;
height: var(--player-timeline-height);
background-color: var(--secondary);
border-radius: 0.25rem;
z-index: 2;
@@ -1827,5 +1872,14 @@
transition: none;
pointer-events: none;
will-change: transform;
padding: 0;
border-radius: 0.25rem;
display: block;
}
.timeline.tooltip canvas {
display: block;
margin-bottom: 0.1rem;
height: 100px;
}
</style>
+25
View File
@@ -2,6 +2,31 @@ import { get } from 'svelte/store';
import type { Image } from './api/model';
import { instanceStore } from './store';
export class ImageCache {
private cache = new Map<string, HTMLImageElement>();
load(src: string): Promise<HTMLImageElement> {
if (this.cache.has(src)) {
return Promise.resolve(this.cache.get(src)!);
}
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
this.cache.set(src, img);
resolve(img);
};
img.onerror = reject;
img.src = src;
});
}
clear() {
this.cache.clear();
}
}
export function getBestThumbnail(
images: Image[] | null,
maxWidthDimension: number = 480,
+113
View File
@@ -0,0 +1,113 @@
import type { StoryBoard } from './api/model';
import { ImageCache } from './images';
export interface TimelineThumbnail {
url: string;
time: number;
width: number;
height: number;
index: number;
}
export async function storyboardThumbnails(
storyboard: StoryBoard,
imageCache: ImageCache,
videoLength: number
): Promise<TimelineThumbnail[]> {
const { count, storyboardCount, width, height, templateUrl } = storyboard;
// 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;
let globalIndex = 0;
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);
if (row >= rows) break;
const time = Math.floor((globalIndex / totalThumbnails) * videoDurationMs);
thumbnails.push({
url,
time,
width,
height,
index: globalIndex
});
globalIndex++;
}
}
return thumbnails;
}
export async function drawTimelineThumbnail(
ctx: CanvasRenderingContext2D,
imageCache: ImageCache,
thumbnails: TimelineThumbnail[],
time: number
): Promise<void> {
if (!thumbnails.length) return;
const timeInMs = time * 1000;
const closest = thumbnails.reduce((prev, curr) =>
Math.abs(curr.time - timeInMs) < Math.abs(prev.time - timeInMs) ? curr : prev
);
if (!closest.url) return;
const img = await imageCache.load(closest.url);
if (!img) {
return;
}
const sheetWidth = img.width;
const sheetHeight = img.height;
const cols = Math.floor(sheetWidth / closest.width);
const rows = Math.floor(sheetHeight / closest.height);
const thumbnailsPerSheet = cols * rows;
const indexInSheet = closest.index % thumbnailsPerSheet;
const thumbX = (indexInSheet % cols) * closest.width;
const thumbY = Math.floor(indexInSheet / cols) * closest.height;
ctx.clearRect(0, 0, closest.width, closest.height);
ctx.drawImage(
img,
thumbX,
thumbY,
closest.width,
closest.height,
0,
0,
closest.width,
closest.height
);
}
+18 -2
View File
@@ -66,7 +66,7 @@
playerState.subscribe(() => {
requestAnimationFrame(() => resetScroll());
})
});
interfaceAmoledTheme.subscribe(async () => {
setAmoledTheme();
@@ -245,6 +245,7 @@
if (isLoggedIn) {
loadNotifications().catch(() => authStore.set(null));
}
resetScroll();
});
@@ -351,6 +352,13 @@
{#if isLoggedIn}
<button class="circle large transparent" onclick={() => ui('#dialog-notifications')}
><i>notifications</i>
<div class="badge">
{#if notifications.length > 99}
99+
{:else}
{notifications.length}
{/if}
</div>
<div class="tooltip bottom">{$_('layout.notifications')}</div>
</button>
{/if}
@@ -401,7 +409,7 @@
{:else}
{#each notifications as notification (notification)}
<article class="no-padding border">
<Thumbnail video={notification}></Thumbnail>
<Thumbnail video={notification} sideways={true}></Thumbnail>
</article>
{/each}
{/if}
@@ -522,6 +530,10 @@
justify-content: center;
}
#dialog-notifications {
width: 500px;
}
@media only screen and (max-width: 993px) {
.pip {
width: 100%;
@@ -529,6 +541,10 @@
right: 0px;
}
#dialog-notifications {
width: 100%;
}
.pip > .pip-info > .player {
height: 80px;
width: 150px;
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.15"
LATEST_VERSION = "1.13.16"
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")