Merge pull request #1334 from Materialious/update/1.13.7

Update/1.13.7
This commit is contained in:
Ward
2026-01-21 08:49:09 +00:00
committed by GitHub
14 changed files with 123 additions and 90 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 190
versionName "1.13.6"
versionCode 191
versionName "1.13.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -89,7 +89,11 @@
<release version="1.13.6" date="2026-1-20">
<release version="1.13.7" date="2026-1-21">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.7</url>
</release>
<release version="1.13.6" date="2026-1-20">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.6</url>
</release>
<release version="1.13.5" date="2026-1-20">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.13.6",
"version": "1.13.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.13.6",
"version": "1.13.7",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.13.6",
"version": "1.13.7",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
@@ -44,4 +44,4 @@
"capacitor",
"electron"
]
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.13.6",
"version": "1.13.7",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
@@ -80,4 +80,4 @@
"youtubei.js": "^16.0.1",
"zod": "^4.3.5"
}
}
}
+53 -34
View File
@@ -61,6 +61,8 @@
interface Props {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
currentTime?: number;
userManualSeeking?: boolean;
isEmbed?: boolean;
playerElement?: HTMLMediaElement | undefined;
showControls?: boolean;
@@ -70,6 +72,8 @@
data,
isEmbed = false,
playerElement = $bindable(undefined),
currentTime = $bindable(0),
userManualSeeking = $bindable(false),
showControls = false
}: Props = $props();
@@ -87,8 +91,7 @@
};
let originalOrigination: ScreenOrientationResult | undefined;
// eslint-disable-next-line no-undef
let watchProgressInterval: NodeJS.Timeout;
let watchProgressInterval: ReturnType<typeof setInterval>;
let showVideoRetry = $state(false);
let androidInitialNetworkStatus: ConnectionStatus | undefined;
@@ -99,7 +102,6 @@
let playerContainer: HTMLElement;
let playerBufferBar: HTMLElement | undefined = $state();
let playerCurrentPlaybackState = $state(false);
let playerCurrentTime = $state(0);
let playerMaxKnownTime = $state(data.video.lengthSeconds);
let playerIsBuffering = $state(true);
let playerVolume = $state(0);
@@ -114,26 +116,24 @@
let playerCloestTimestamp: Timestamp | undefined = $state();
let playerCloestSponsor: Segment | undefined = $state();
let playerSliderElement: HTMLElement | undefined = $state();
// eslint-disable-next-line no-undef
let playerSliderDebounce: NodeJS.Timeout;
let playerSliderDebounce: ReturnType<typeof setTimeout>;
let playerVolumeElement: HTMLElement | undefined = $state();
let playerUserManualSeeking: boolean = $state(false);
let playerIsFullscreen: boolean = $state(false);
const playerTimelineSlider = new Slider({
min: 0,
step: 0.1,
value: () => playerCurrentTime,
value: () => currentTime,
onValueChange: (timeToSet) => {
playerUserManualSeeking = true;
playerCurrentTime = timeToSet;
userManualSeeking = true;
currentTime = timeToSet;
if (playerSliderDebounce) clearTimeout(playerSliderDebounce);
playerSliderDebounce = setTimeout(() => {
if (playerElement) {
playerElement.currentTime = timeToSet;
playerUserManualSeeking = false;
userManualSeeking = false;
}
}, 300);
},
@@ -151,14 +151,12 @@
step: 0.1
});
// eslint-disable-next-line no-undef
let playerAndroidUITimeout: NodeJS.Timeout;
let playerAndroidUITimeout: ReturnType<typeof setTimeout>;
let clickCount = $state(0);
// eslint-disable-next-line no-undef
let clickCounterTimeout: NodeJS.Timeout;
let clickCounterTimeout: ReturnType<typeof setTimeout>;
let seekDirection: 'forwards' | 'backwards' = $state('forwards');
let seekDirection: 'forwards' | 'backwards' | undefined = $state();
const STORAGE_KEY_VOLUME = 'shaka-preferred-volume';
playertheatreModeIsActive.subscribe(async () => {
@@ -438,6 +436,8 @@
if (document.fullscreenElement) {
document.exitFullscreen();
playerIsFullscreen = false;
setTimeout(() => updateVideoPlayerHeight(), 100);
} else {
playerContainer.requestFullscreen();
playerIsFullscreen = true;
@@ -583,10 +583,23 @@
// we calaculate player height to then allow children pages
// to wrap around it.
function updateVideoPlayerHeight() {
if (playerContainer) {
const height = playerContainer.getBoundingClientRect().height;
document.documentElement.style.setProperty('--video-player-height', `${height + 10}px`);
if (!playerContainer) {
return;
}
const height = playerContainer.getBoundingClientRect().height;
document.documentElement.style.setProperty('--video-player-height', `${height + 10}px`);
}
let showPlayerUiTimeout: ReturnType<typeof setTimeout>;
function showPlayerUI() {
showControls = true;
if (showPlayerUiTimeout) clearTimeout(showPlayerUiTimeout);
showPlayerUiTimeout = setTimeout(() => {
showControls = false;
}, 5000);
}
onMount(async () => {
@@ -901,11 +914,11 @@
playerElement?.addEventListener('timeupdate', () => {
if (!playerElement) return;
if (!playerUserManualSeeking) {
playerCurrentTime = playerElement.currentTime ?? 0;
if (!userManualSeeking) {
currentTime = playerElement.currentTime ?? 0;
}
if (playerMaxKnownTime === 0 || playerCurrentTime > playerMaxKnownTime) {
if (playerMaxKnownTime === 0 || currentTime > playerMaxKnownTime) {
playerMaxKnownTime = Number(playerElement.currentTime);
}
@@ -924,7 +937,7 @@
playerBufferedTo = buffered.end(0);
const bufferedPercent = (playerBufferedTo / playerMaxKnownTime) * 100;
const progressPercent = (playerCurrentTime / playerMaxKnownTime) * 100;
const progressPercent = (currentTime / playerMaxKnownTime) * 100;
const bufferAhead = Math.max(0, bufferedPercent - progressPercent);
@@ -1060,6 +1073,8 @@
currentTarget: EventTarget & HTMLDivElement;
}
) {
seekDirection = undefined;
if (Capacitor.getPlatform() === 'android') {
const initalControlsState = showControls.valueOf();
@@ -1109,7 +1124,7 @@
} else {
seekDirection = 'forwards';
playerElement.currentTime = Math.min(
playerElement.duration,
playerMaxKnownTime,
playerElement.currentTime + playerDoubleTapSeek
);
}
@@ -1145,7 +1160,7 @@
Mousetrap.unbind(['left', 'right', 'space', 'c', 'f', 'shift+left', 'shift+right', 'enter']);
if (watchProgressInterval) clearTimeout(watchProgressInterval);
if (watchProgressInterval) clearInterval(watchProgressInterval);
if (sabrAdapter) sabrAdapter.dispose();
@@ -1167,7 +1182,9 @@
class:hide={showVideoRetry}
role="presentation"
onclick={onVideoClick}
onmouseenter={() => (showControls = true)}
onmouseenter={showPlayerUI}
onmousemove={showPlayerUI}
onscroll={showPlayerUI}
onmouseleave={() => (showControls = false)}
bind:this={playerContainer}
>
@@ -1188,7 +1205,7 @@
{#if data.video.liveNow}
{$_('thumbnail.live')}
{:else}
{videoLength(playerCurrentTime)} / {videoLength(playerMaxKnownTime)}
{videoLength(currentTime)} / {videoLength(playerMaxKnownTime)}
{/if}
</p>
<p class="chip inverse-primary">
@@ -1233,12 +1250,13 @@
<div id="player-controls" transition:fade>
<div
class="player-slider full-width"
class:disable-tv={$isAndroidTvStore}
{...playerTimelineSlider.root}
onmousemove={timelineMouseMove}
bind:this={playerSliderElement}
>
<div class="track">
{#if !playerUserManualSeeking}
{#if !userManualSeeking}
<div class="tooltip" style="position: absolute;left: var(--timeline-tooltip-left);">
{#if playerCloestSponsor}
{sponsorSegments[playerCloestSponsor.category]}
@@ -1254,15 +1272,11 @@
<div class="range"></div>
<div {...playerTimelineSlider.thumb}>
<div class="tooltip thumb-tooltip">
{videoLength(playerCurrentTime)}
{videoLength(currentTime)}
</div>
</div>
</div>
<div
bind:this={playerBufferBar}
class="buffered-bar"
class:hide={playerUserManualSeeking}
></div>
<div bind:this={playerBufferBar} class="buffered-bar" class:hide={userManualSeeking}></div>
{#each data.content.timestamps as chapter, index (chapter)}
<div
class="chapter-marker"
@@ -1318,7 +1332,7 @@
{#if data.video.liveNow}
{$_('thumbnail.live')}
{:else}
{videoLength(playerCurrentTime)} / {videoLength(data.video.lengthSeconds)}
{videoLength(currentTime)} / {videoLength(data.video.lengthSeconds)}
{/if}
</p>
{#if !$isAndroidTvStore}
@@ -1670,6 +1684,11 @@
background-color: var(--tertiary);
}
.disable-tv {
pointer-events: none;
cursor: not-allowed;
}
menu.mobile {
transform: scale(1) translateY(-40%) translateX(0);
width: 300px;
@@ -25,8 +25,7 @@
let showSearchBox = $state(false);
// eslint-disable-next-line no-undef
let debounceTimer: NodeJS.Timeout;
let debounceTimer: ReturnType<typeof setTimeout>;
function debouncedSearch(event: any) {
if (!$interfaceSearchSuggestionsStore) return;
@@ -11,8 +11,7 @@
let suggestionsForSearch: string[] = $state([]);
let search = $state('');
// eslint-disable-next-line no-undef
let debounceTimer: NodeJS.Timeout;
let debounceTimer: ReturnType<typeof setTimeout>;
function debouncedSearch(event: any) {
if (!$interfaceSearchSuggestionsStore) return;
@@ -67,8 +67,7 @@
let playerCurrentTime: number = $state(0);
let premiereTime = $state('');
// eslint-disable-next-line no-undef
let premiereUpdateInterval: NodeJS.Timeout;
let premiereUpdateInterval: ReturnType<typeof setTimeout>;
if (
!data.video.premiereTimestamp &&
@@ -362,8 +361,7 @@
playertheatreModeIsActive.set(!$playertheatreModeIsActive);
}
// eslint-disable-next-line no-undef
let pauseTimeout: NodeJS.Timeout | undefined = $state();
let pauseTimeout: ReturnType<typeof setTimeout> | undefined = $state();
function setPauseTimer() {
if (pauseTimeout) {
clearTimeout(pauseTimeout);
@@ -18,46 +18,51 @@
let showInfo = $state(false);
let playerCurrentTime: number = $state(0);
let showControls = $state(false);
let currentTime = $state(0);
let userManualSeeking = $state(false);
// eslint-disable-next-line no-undef
let seekInterval: NodeJS.Timeout | undefined;
let seekStartTime: number = 0;
let seekRaf: number | null = null;
let seekStartTime = 0;
let isSeeking = false;
let lastUpdate = 0;
function startSeeking(direction: 'left' | 'right') {
showControls = true;
if (!playerElement || showInfo || isSeeking) return true;
userManualSeeking = true;
isSeeking = true;
showControls = true;
seekStartTime = Date.now();
lastUpdate = performance.now();
const videoDuration = data.video.lengthSeconds;
const baseSeekAmount = videoDuration * 0.005; // 0.5% of video duration as base
const duration = data.video.lengthSeconds;
const base = duration * 0.005; // ~0.5%
// Initial seek
if (direction === 'right') {
playerElement.currentTime = Math.min(
playerElement.currentTime + baseSeekAmount,
videoDuration
);
} else {
playerElement.currentTime = Math.max(playerElement.currentTime - baseSeekAmount, 0);
function step(now: number) {
if (!isSeeking) return;
const elapsedTotal = (now - seekStartTime) / 1000; // total hold time
const throttleDelta = now - lastUpdate;
// only update every ~100ms so we dont overshoot
if (throttleDelta > 100) {
const factor = 1 + Math.min(elapsedTotal ** 1.2, 3); // gentler acceleration
const amount = base * factor;
if (direction === 'right') {
currentTime = Math.min(currentTime + amount, duration);
} else {
currentTime = Math.max(currentTime - amount, 0);
}
lastUpdate = now;
}
seekRaf = requestAnimationFrame(step);
}
// Progressive seeking
seekInterval = setInterval(() => {
if (!playerElement || !isSeeking) return;
const holdDuration = (Date.now() - seekStartTime) / 1000; // in seconds
const acceleration = Math.min(holdDuration * 0.5, 5); // Cap at 5x acceleration
const seekAmount = baseSeekAmount * (1 + acceleration);
if (direction === 'right') {
playerElement.currentTime = Math.min(playerElement.currentTime + seekAmount, videoDuration);
} else {
playerElement.currentTime = Math.max(playerElement.currentTime - seekAmount, 0);
}
}, 100);
seekStartTime = performance.now();
seekRaf = requestAnimationFrame(step);
return false;
}
@@ -65,11 +70,16 @@
function stopSeeking() {
isSeeking = false;
showControls = false;
if (seekInterval) {
clearInterval(seekInterval);
seekInterval = undefined;
userManualSeeking = false;
if (seekRaf !== null) {
cancelAnimationFrame(seekRaf);
seekRaf = null;
}
if (playerElement) {
playerElement.currentTime = currentTime;
}
return false;
}
onMount(() => {
@@ -150,14 +160,18 @@
onDestroy(() => {
Mousetrap.unbind(['up', 'down', 'left', 'right', 'enter']);
if (seekInterval) {
clearInterval(seekInterval);
}
});
</script>
{#key data.video.videoId}
<Player bind:playerElement isEmbed={true} {data} {showControls} />
<Player
bind:playerElement
bind:currentTime
bind:userManualSeeking
isEmbed={true}
{data}
{showControls}
/>
{/key}
{#if showInfo}
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.13.6"
LATEST_VERSION = "1.13.7"
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")