@@ -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">
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 don’t 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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user