@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 177
|
||||
versionName "1.12.5"
|
||||
versionCode 178
|
||||
versionName "1.12.6"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -74,7 +74,11 @@
|
||||
|
||||
|
||||
|
||||
<release version="1.12.5" date="2025-12-13">
|
||||
|
||||
<release version="1.12.6" date="2025-12-22">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.12.6</url>
|
||||
</release>
|
||||
<release version="1.12.5" date="2025-12-13">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.12.5</url>
|
||||
</release>
|
||||
<release version="1.12.4" date="2025-12-11">
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.12.5",
|
||||
"version": "1.12.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.12.5",
|
||||
"version": "1.12.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.12.5",
|
||||
"version": "1.12.6",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.12.5",
|
||||
"version": "1.12.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -75,4 +75,4 @@
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"youtubei.js": "^16.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@
|
||||
import ui from 'beercss';
|
||||
import ISO6391 from 'iso-639-1';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import 'shaka-player/dist/controls.css';
|
||||
import '$lib/css/shaka-player-theme.css';
|
||||
import { CapacitorMusicControls } from 'capacitor-music-controls-plugin';
|
||||
import shaka from 'shaka-player/dist/shaka-player.ui';
|
||||
import { SponsorBlock, type Category, type Segment } from 'sponsorblock-api';
|
||||
@@ -22,9 +20,9 @@
|
||||
import type { VideoPlay } from '../api/model';
|
||||
import {
|
||||
authStore,
|
||||
darkModeStore,
|
||||
instanceStore,
|
||||
isAndroidTvStore,
|
||||
playerAlwaysLoopStore,
|
||||
playerAndroidLockOrientation,
|
||||
playerAutoPlayStore,
|
||||
playerCCByDefault,
|
||||
@@ -34,31 +32,36 @@
|
||||
playerProxyVideosStore,
|
||||
playerSavePlaybackPositionStore,
|
||||
playerState,
|
||||
playerStatisticsByDefault,
|
||||
playertheatreModeIsActive,
|
||||
playerYouTubeJsFallback,
|
||||
sponsorBlockCategoriesStore,
|
||||
sponsorBlockDisplayToastStore,
|
||||
sponsorBlockStore,
|
||||
sponsorBlockUrlStore,
|
||||
synciousInstanceStore,
|
||||
synciousStore,
|
||||
themeColorStore
|
||||
synciousStore
|
||||
} from '../store';
|
||||
import { getDynamicTheme, setStatusBarColor } from '../theme';
|
||||
import { setStatusBarColor } from '../theme';
|
||||
import { patchYoutubeJs } from '$lib/patches/youtubejs';
|
||||
import { goToNextVideo, goToPreviousVideo, playbackRates } from '$lib/player';
|
||||
import { EndTimeElement } from '$lib/shaka-elements/endTime';
|
||||
import { dashManifestDomainInclusion } from '$lib/android/youtube/dash';
|
||||
import { injectSabr } from '$lib/sabr';
|
||||
import type { SabrStreamingAdapter } from 'googlevideo/sabr-streaming-adapter';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
interface Props {
|
||||
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
|
||||
isEmbed?: boolean;
|
||||
playerElement?: HTMLMediaElement | undefined;
|
||||
hideControls?: boolean;
|
||||
}
|
||||
|
||||
let { data, isEmbed = false, playerElement = $bindable(undefined) }: Props = $props();
|
||||
let {
|
||||
data,
|
||||
isEmbed = false,
|
||||
playerElement = $bindable(undefined),
|
||||
hideControls = false
|
||||
}: Props = $props();
|
||||
|
||||
let segments: Segment[] = [];
|
||||
|
||||
@@ -69,23 +72,27 @@
|
||||
let showVideoRetry = $state(false);
|
||||
|
||||
let player: shaka.Player;
|
||||
let shakaUi: shaka.ui.Overlay;
|
||||
let sabrAdapter: SabrStreamingAdapter | null;
|
||||
let playerContainer: HTMLElement;
|
||||
|
||||
let playerCurrentPlaybackState = $state(false);
|
||||
let playerCurrentTime = $state(0);
|
||||
let playerMaxKnownTime = $state(0);
|
||||
let playerIsSeeking = $state(false);
|
||||
let playerIsBuffering = $state(false);
|
||||
let playerVolume = $state(0);
|
||||
let playerSettings: 'quality' | 'speed' | 'language' | 'root' = $state('root');
|
||||
let playerTextTracks: shaka.extern.TextTrack[] | undefined = $state(undefined);
|
||||
let playerCurrentVideoTrack: shaka.extern.VideoTrack | undefined = $state(undefined);
|
||||
let playerCurrentAudioTrack: shaka.extern.AudioTrack | undefined = $state(undefined);
|
||||
let playerLoop = $state($playerAlwaysLoopStore);
|
||||
|
||||
const STORAGE_KEY_VOLUME = 'shaka-preferred-volume';
|
||||
|
||||
async function updateSeekBarTheme() {
|
||||
if (!shakaUi) return;
|
||||
playertheatreModeIsActive.subscribe(async () => {
|
||||
await tick();
|
||||
shakaUi.configure({
|
||||
seekBarColors: {
|
||||
played: (await getDynamicTheme())['--primary']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
themeColorStore.subscribe(updateSeekBarTheme);
|
||||
darkModeStore.subscribe(updateSeekBarTheme);
|
||||
updateVideoPlayerHeight();
|
||||
});
|
||||
|
||||
function restoreDefaultLanguage() {
|
||||
if (!$playerDefaultLanguage || $playerDefaultLanguage === 'original') {
|
||||
@@ -109,8 +116,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
function filterUniqueAudioTracks(tracks: shaka.extern.AudioTrack[]): shaka.extern.AudioTrack[] {
|
||||
const uniqueTracks: shaka.extern.AudioTrack[] = [];
|
||||
const seen = new SvelteSet<string>();
|
||||
|
||||
for (const track of tracks) {
|
||||
const identifier = `${track.language}-${track.label || 'No Label'}`;
|
||||
if (!seen.has(identifier)) {
|
||||
seen.add(identifier);
|
||||
uniqueTracks.push(track);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueTracks;
|
||||
}
|
||||
|
||||
function saveVolumePreference() {
|
||||
if (!playerElement) return;
|
||||
playerVolume = playerElement.volume;
|
||||
localStorage.setItem(STORAGE_KEY_VOLUME, playerElement.volume.toString());
|
||||
}
|
||||
|
||||
@@ -118,6 +141,7 @@
|
||||
const savedVolume = localStorage.getItem(STORAGE_KEY_VOLUME);
|
||||
if (savedVolume && playerElement) {
|
||||
playerElement.volume = parseFloat(savedVolume);
|
||||
playerVolume = playerElement.volume;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,20 +322,26 @@
|
||||
|
||||
page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate));
|
||||
|
||||
function setActiveVideoTrack() {
|
||||
const videoTracks = player.getVideoTracks();
|
||||
playerCurrentVideoTrack = videoTracks.find((track) => track.active);
|
||||
}
|
||||
|
||||
function setActiveAudioTrack() {
|
||||
const audioTracks = player.getAudioTracks();
|
||||
playerCurrentAudioTrack = audioTracks.find((track) => track.active);
|
||||
}
|
||||
|
||||
async function loadVideo() {
|
||||
showVideoRetry = false;
|
||||
|
||||
sabrAdapter = await injectSabr(data.video, player);
|
||||
|
||||
try {
|
||||
document.getElementsByClassName('shaka-ad-info')[0].remove();
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
player.addEventListener('loaded', () => {
|
||||
restoreQualityPreference();
|
||||
restoreDefaultLanguage();
|
||||
setActiveVideoTrack();
|
||||
setActiveAudioTrack();
|
||||
|
||||
if ($playerCCByDefault) {
|
||||
toggleSubtitles();
|
||||
@@ -421,14 +451,6 @@
|
||||
if ($playerDefaultPlaybackSpeed && playerElement) {
|
||||
playerElement.playbackRate = $playerDefaultPlaybackSpeed;
|
||||
}
|
||||
|
||||
if ($playerStatisticsByDefault) {
|
||||
// Appears to be no native way in shaka to toggle statistics on and off
|
||||
const shakaStatisticsButton = document.querySelector('.shaka-statistics-button') as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
shakaStatisticsButton?.click();
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadVideo() {
|
||||
@@ -437,6 +459,24 @@
|
||||
await loadVideo();
|
||||
}
|
||||
|
||||
function toggleVideoPlaybackStatus() {
|
||||
if (playerCurrentPlaybackState) {
|
||||
playerElement?.pause();
|
||||
} else {
|
||||
playerElement?.play();
|
||||
}
|
||||
}
|
||||
|
||||
// Due to how our player is rendered in layout for stateful pip
|
||||
// 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`);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
shaka.polyfill.installAll();
|
||||
if (!shaka.Player.isBrowserSupported()) {
|
||||
@@ -466,66 +506,22 @@
|
||||
});
|
||||
playerElement = document.getElementById('player') as HTMLMediaElement;
|
||||
|
||||
playerElement.loop = playerLoop;
|
||||
|
||||
if ($playerState) {
|
||||
playerState.set({ ...$playerState, playerElement: playerElement });
|
||||
}
|
||||
|
||||
// Due to how our player is rendered in layout for stateful pip
|
||||
// we calaculate player height to then allow children pages
|
||||
// to wrap around it.
|
||||
function updateVideoPlayerHeight() {
|
||||
const container = document.getElementById('shaka-container') as HTMLElement;
|
||||
|
||||
if (container) {
|
||||
const height = container.getBoundingClientRect().height;
|
||||
document.documentElement.style.setProperty('--video-player-height', `${height + 10}px`);
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', updateVideoPlayerHeight);
|
||||
updateVideoPlayerHeight();
|
||||
|
||||
// Change instantly to stop video from being loud for a second
|
||||
restoreVolumePreference();
|
||||
|
||||
const playerContainer = document.getElementById('shaka-container') as HTMLElement;
|
||||
playerContainer = document.getElementById('player-container') as HTMLElement;
|
||||
|
||||
shakaUi = new shaka.ui.Overlay(player, playerContainer, playerElement);
|
||||
window.addEventListener('resize', updateVideoPlayerHeight);
|
||||
updateVideoPlayerHeight();
|
||||
|
||||
await player.attach(playerElement);
|
||||
|
||||
shaka.ui.Controls.registerElement('end_time', {
|
||||
create: (parent: HTMLElement, controls: shaka.ui.Controls) => {
|
||||
return new EndTimeElement(parent, controls);
|
||||
}
|
||||
});
|
||||
|
||||
shakaUi.configure({
|
||||
addBigPlayButton: Capacitor.getPlatform() === 'android',
|
||||
controlPanelElements: [
|
||||
'play_pause',
|
||||
Capacitor.getPlatform() === 'android' ? '' : 'volume',
|
||||
'spacer',
|
||||
'time_and_duration',
|
||||
data.video.liveNow || data.video.lengthSeconds < 240 ? '' : 'end_time',
|
||||
'captions',
|
||||
'overflow_menu',
|
||||
'fullscreen'
|
||||
],
|
||||
overflowMenuButtons: [
|
||||
'cast',
|
||||
'airplay',
|
||||
'quality',
|
||||
'playback_rate',
|
||||
'loop',
|
||||
'language',
|
||||
'statistics'
|
||||
],
|
||||
playbackRates: playbackRates,
|
||||
enableTooltips: false
|
||||
});
|
||||
|
||||
updateSeekBarTheme();
|
||||
|
||||
player?.addEventListener('error', (event) => {
|
||||
const error = (event as CustomEvent).detail as shaka.util.Error;
|
||||
console.error('Player error:', error);
|
||||
@@ -557,16 +553,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Required to stop buttons from being still selected when fullscreening
|
||||
document?.addEventListener('fullscreenchange', async () => {
|
||||
const buttons = document.querySelectorAll('.shaka-controls-button-panel button');
|
||||
buttons.forEach((button) => {
|
||||
// Reset the button's focus and active states
|
||||
(button as HTMLElement).blur(); // Remove focus from the button
|
||||
button.removeAttribute('aria-pressed'); // Reset any ARIA attributes that might indicate selection
|
||||
});
|
||||
});
|
||||
|
||||
playerElement?.addEventListener('volumechange', saveVolumePreference);
|
||||
|
||||
await androidHandleRotate();
|
||||
@@ -710,17 +696,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
const volumeContainer = playerContainer.getElementsByClassName('shaka-volume-bar-container');
|
||||
volumeContainer[0]?.addEventListener('mousewheel', (event) => {
|
||||
event.preventDefault();
|
||||
const delta = Math.sign((event as any).deltaY);
|
||||
const newVolume = Math.max(
|
||||
0,
|
||||
Math.min(1, (playerElement as HTMLMediaElement).volume - delta * 0.05)
|
||||
);
|
||||
(playerElement as HTMLMediaElement).volume = newVolume;
|
||||
});
|
||||
|
||||
Mousetrap.bind('c', () => {
|
||||
toggleSubtitles();
|
||||
return false;
|
||||
@@ -730,7 +705,7 @@
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
shakaUi.getControls()?.toggleFullScreen();
|
||||
playerContainer.requestFullscreen();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -765,14 +740,49 @@
|
||||
playerElement.currentTime += frameTime;
|
||||
});
|
||||
|
||||
const volumeContainer = document.getElementById('volume-slider');
|
||||
volumeContainer?.addEventListener('mousewheel', (event) => {
|
||||
event.preventDefault();
|
||||
const delta = Math.sign((event as any).deltaY);
|
||||
const newVolume = Math.max(
|
||||
0,
|
||||
Math.min(1, (playerElement as HTMLMediaElement).volume - delta * 0.05)
|
||||
);
|
||||
(playerElement as HTMLMediaElement).volume = newVolume;
|
||||
playerVolume = newVolume;
|
||||
});
|
||||
|
||||
playerElement?.addEventListener('pause', async () => {
|
||||
playerCurrentPlaybackState = false;
|
||||
playerIsBuffering = false;
|
||||
savePlayerPos();
|
||||
});
|
||||
|
||||
playerElement?.addEventListener('ended', async () => {
|
||||
playerCurrentPlaybackState = false;
|
||||
playerIsBuffering = false;
|
||||
await goToNextVideo(data.video, data.playlistId);
|
||||
});
|
||||
|
||||
playerElement?.addEventListener('playing', () => {
|
||||
playerCurrentPlaybackState = true;
|
||||
playerIsBuffering = false;
|
||||
});
|
||||
|
||||
playerElement?.addEventListener('waiting', () => {
|
||||
playerCurrentPlaybackState = false;
|
||||
playerIsBuffering = true;
|
||||
});
|
||||
|
||||
playerElement?.addEventListener('timeupdate', () => {
|
||||
if (playerIsSeeking) return;
|
||||
playerCurrentTime = playerElement?.currentTime ?? 0;
|
||||
|
||||
if (playerMaxKnownTime === 0 || playerCurrentTime > playerMaxKnownTime) {
|
||||
playerMaxKnownTime = Number(playerElement?.currentTime);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await loadVideo();
|
||||
} catch (error: unknown) {
|
||||
@@ -788,6 +798,11 @@
|
||||
await reloadVideo();
|
||||
}
|
||||
}
|
||||
|
||||
// Update video player height again on video loaded.
|
||||
updateVideoPlayerHeight();
|
||||
|
||||
playerTextTracks = player.getTextTracks();
|
||||
});
|
||||
|
||||
async function getLastPlayPos(): Promise<number> {
|
||||
@@ -869,6 +884,8 @@
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', updateVideoPlayerHeight);
|
||||
|
||||
Mousetrap.unbind(['left', 'right', 'space', 'c', 'f', 'shift+left', 'shift+right']);
|
||||
|
||||
if (watchProgressTimeout) {
|
||||
@@ -883,20 +900,24 @@
|
||||
player.unload();
|
||||
player.destroy();
|
||||
}
|
||||
|
||||
if (shakaUi) {
|
||||
shakaUi.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="shaka-container"
|
||||
class="player-theme"
|
||||
id="player-container"
|
||||
class:contain-video={!$isAndroidTvStore}
|
||||
class:tv-contain-video={$isAndroidTvStore}
|
||||
data-shaka-player-container
|
||||
class:hide={showVideoRetry}
|
||||
role="presentation"
|
||||
onclick={(event) => {
|
||||
if (
|
||||
event.target &&
|
||||
event.target instanceof HTMLElement &&
|
||||
event.target.id === 'player-center'
|
||||
) {
|
||||
toggleVideoPlaybackStatus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<video
|
||||
controls={false}
|
||||
@@ -909,6 +930,276 @@
|
||||
{data.video.title}
|
||||
</div>
|
||||
{/if}
|
||||
<p id="mobile-time" class="chip primary s">
|
||||
{#if data.video.liveNow}
|
||||
{$_('thumbnail.live')}
|
||||
{:else}
|
||||
{videoLength(playerCurrentTime)} / {videoLength(data.video.lengthSeconds)}
|
||||
{/if}
|
||||
</p>
|
||||
<div id="player-center">
|
||||
{#if playerIsBuffering}
|
||||
<progress class="circle large indeterminate" value="50" max="100"></progress>
|
||||
{:else if !playerCurrentPlaybackState}
|
||||
<button class="extra" onclick={toggleVideoPlaybackStatus}>
|
||||
<i>play_arrow</i>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !hideControls}
|
||||
<div id="player-controls">
|
||||
<article class="round" style="width: 100%;padding: 0;height: 10px;">
|
||||
<label class="slider max">
|
||||
<input
|
||||
oninput={() => {
|
||||
playerIsSeeking = true;
|
||||
if (playerElement) playerElement.currentTime = playerCurrentTime;
|
||||
}}
|
||||
onchange={() => (playerIsSeeking = false)}
|
||||
type="range"
|
||||
min="0"
|
||||
bind:value={playerCurrentTime}
|
||||
max={data.video.liveNow ? playerMaxKnownTime : data.video.lengthSeconds}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</article>
|
||||
|
||||
<nav>
|
||||
<nav class="no-wrap">
|
||||
<button onclick={toggleVideoPlaybackStatus}>
|
||||
<i>
|
||||
{#if playerCurrentPlaybackState}
|
||||
pause
|
||||
{:else}
|
||||
play_arrow
|
||||
{/if}
|
||||
</i>
|
||||
</button>
|
||||
{#if Capacitor.getPlatform() !== 'android'}
|
||||
<article
|
||||
id="volume-slider"
|
||||
class="round m l"
|
||||
style="padding: 0;height: 10px;width: 150px;"
|
||||
>
|
||||
<label class="slider max">
|
||||
<input
|
||||
oninput={() => {
|
||||
if (!playerElement) return;
|
||||
|
||||
playerElement.volume = playerVolume;
|
||||
}}
|
||||
bind:value={playerVolume}
|
||||
type="range"
|
||||
step="0.1"
|
||||
max="1"
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</article>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<div class="max"></div>
|
||||
|
||||
<nav class="no-wrap">
|
||||
<p class="chip primary m l" style="border: none;">
|
||||
{#if data.video.liveNow}
|
||||
{$_('thumbnail.live')}
|
||||
{:else}
|
||||
{videoLength(playerCurrentTime)} / {videoLength(data.video.lengthSeconds)}
|
||||
{/if}
|
||||
</p>
|
||||
{#if playerTextTracks && playerTextTracks.length > 0 && !data.video.liveNow}
|
||||
<button>
|
||||
<i>closed_caption</i>
|
||||
<menu class="no-wrap mobile" id="cc-menu" data-ui="#cc-menu">
|
||||
<li
|
||||
role="presentation"
|
||||
data-ui="#cc-menu"
|
||||
onclick={() => player.setTextTrackVisibility(false)}
|
||||
>
|
||||
{$_('player.controls.off')}
|
||||
</li>
|
||||
{#each playerTextTracks as track (track)}
|
||||
<li
|
||||
role="presentation"
|
||||
data-ui="#cc-menu"
|
||||
onclick={() => {
|
||||
player.selectTextTrack(track);
|
||||
player.setTextTrackVisibility(true);
|
||||
}}
|
||||
>
|
||||
{track.label}
|
||||
</li>
|
||||
{/each}
|
||||
</menu>
|
||||
</button>
|
||||
{/if}
|
||||
<button>
|
||||
<i>settings</i>
|
||||
<menu class="no-wrap mobile" id="settings-menu">
|
||||
{#if playerSettings !== 'root'}
|
||||
<li role="presentation" onclick={() => (playerSettings = 'root')}>
|
||||
<i>arrow_back</i>
|
||||
{$_('player.controls.back')}
|
||||
</li>
|
||||
{/if}
|
||||
{#if playerSettings === 'root'}
|
||||
<li role="presentation" onclick={() => (playerSettings = 'quality')}>
|
||||
<nav class="no-wrap" style="width: 100%;">
|
||||
<i>high_quality</i>
|
||||
{$_('player.controls.quality')}
|
||||
|
||||
<div class="max"></div>
|
||||
|
||||
<span class="chip">
|
||||
{#if playerCurrentVideoTrack}
|
||||
{playerCurrentVideoTrack.height}p
|
||||
{:else}
|
||||
{$_('player.controls.auto')}
|
||||
{/if}
|
||||
</span>
|
||||
</nav>
|
||||
</li>
|
||||
<li role="presentation" onclick={() => (playerSettings = 'speed')}>
|
||||
<nav class="no-wrap" style="width: 100%;">
|
||||
<i>speed</i>
|
||||
{$_('player.controls.playbackSpeed')}
|
||||
|
||||
<div class="max"></div>
|
||||
|
||||
<span class="chip">
|
||||
{playerElement?.playbackRate}x
|
||||
</span>
|
||||
</nav>
|
||||
</li>
|
||||
{#if playerCurrentAudioTrack && playerCurrentAudioTrack.label !== null}
|
||||
<li role="presentation" onclick={() => (playerSettings = 'language')}>
|
||||
<nav class="no-wrap" style="width: 100%;">
|
||||
<i>language</i>
|
||||
{$_('player.controls.language')}
|
||||
|
||||
<div class="max"></div>
|
||||
|
||||
<span class="chip">
|
||||
{#if playerCurrentAudioTrack}
|
||||
{playerCurrentAudioTrack.language !== 'und'
|
||||
? ISO6391.getName(playerCurrentAudioTrack.language)
|
||||
: playerCurrentAudioTrack.label}
|
||||
{/if}
|
||||
</span>
|
||||
</nav>
|
||||
</li>
|
||||
{/if}
|
||||
<li
|
||||
role="presentation"
|
||||
onclick={() => {
|
||||
if (playerElement) playerElement.loop = !playerLoop;
|
||||
playerLoop = !playerLoop;
|
||||
}}
|
||||
>
|
||||
<nav class="no-wrap" style="width: 100%;">
|
||||
<i>all_inclusive</i>
|
||||
{$_('player.controls.loop')}
|
||||
|
||||
<div class="max"></div>
|
||||
|
||||
<span class="chip">
|
||||
{playerLoop ? $_('player.controls.on') : $_('player.controls.off')}
|
||||
</span>
|
||||
</nav>
|
||||
</li>
|
||||
{:else if playerSettings === 'quality'}
|
||||
<li
|
||||
role="presentation"
|
||||
onclick={() => {
|
||||
playerSettings = 'root';
|
||||
player.configure({ abr: true });
|
||||
playerCurrentVideoTrack = undefined;
|
||||
}}
|
||||
>
|
||||
{$_('player.controls.auto')}
|
||||
</li>
|
||||
{#each player.getVideoTracks().sort((a, b) => {
|
||||
const heightA = a.height || 0;
|
||||
const heightB = b.height || 0;
|
||||
const widthA = a.width || 0;
|
||||
const widthB = b.width || 0;
|
||||
return heightB - heightA || widthB - widthA;
|
||||
}) as track (track)}
|
||||
<li
|
||||
role="presentation"
|
||||
onclick={() => {
|
||||
playerSettings = 'root';
|
||||
player.selectVideoTrack(track, true);
|
||||
setActiveVideoTrack();
|
||||
}}
|
||||
>
|
||||
{track.height}p
|
||||
</li>
|
||||
{/each}
|
||||
{:else if playerSettings === 'speed'}
|
||||
{#each playbackRates as playbackRate (playbackRate)}
|
||||
<li
|
||||
role="presentation"
|
||||
onclick={() => {
|
||||
playerSettings = 'root';
|
||||
if (playerElement) playerElement.playbackRate = playbackRate;
|
||||
}}
|
||||
>
|
||||
{playbackRate}
|
||||
</li>
|
||||
{/each}
|
||||
{:else if playerSettings === 'language'}
|
||||
{#each filterUniqueAudioTracks(player.getAudioTracks()) as track (track)}
|
||||
<li
|
||||
role="presentation"
|
||||
onclick={() => {
|
||||
playerSettings = 'root';
|
||||
player.selectAudioTrack(track);
|
||||
setActiveAudioTrack();
|
||||
}}
|
||||
>
|
||||
{#if track.language !== 'und'}
|
||||
{ISO6391.getName(track.language)} -
|
||||
{/if}
|
||||
{track.label}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</menu>
|
||||
</button>
|
||||
{#if document.pictureInPictureEnabled}
|
||||
<button
|
||||
onclick={() => {
|
||||
(playerElement as HTMLVideoElement).requestPictureInPicture();
|
||||
}}
|
||||
>
|
||||
<i>pip</i>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
playerContainer.requestFullscreen();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i>
|
||||
{#if document.fullscreenElement}
|
||||
fullscreen_exit
|
||||
{:else}
|
||||
fullscreen
|
||||
{/if}
|
||||
</i>
|
||||
</button>
|
||||
</nav>
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showVideoRetry}
|
||||
@@ -938,6 +1229,59 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#player-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#player-controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 2s ease;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#player-center {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#mobile-time {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transition: opacity 2s ease;
|
||||
}
|
||||
|
||||
#player-container:focus-within #player-controls,
|
||||
#player-container:active #player-controls,
|
||||
#player-container:hover #player-controls {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
#player-container:focus-within #mobile-time,
|
||||
#player-container:active #mobile-time,
|
||||
#player-container:hover #mobile-time {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
menu.mobile {
|
||||
transform: scale(1) translateY(-40%) translateX(0);
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.contain-video {
|
||||
max-height: 80vh;
|
||||
max-width: calc(80vh * 16 / 9);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
interfaceLowBandwidthMode,
|
||||
isAndroidTvStore,
|
||||
playerSavePlaybackPositionStore,
|
||||
playerState,
|
||||
syncPartyConnectionsStore,
|
||||
syncPartyPeerStore,
|
||||
synciousInstanceStore,
|
||||
@@ -119,7 +120,9 @@
|
||||
removeEventListener('resize', disableSideways);
|
||||
});
|
||||
|
||||
function syncChangeVideo() {
|
||||
function onVideoSelected() {
|
||||
playerState.set(undefined);
|
||||
|
||||
if ($syncPartyConnectionsStore) {
|
||||
const events = {
|
||||
events: [{ type: 'change-video', videoId: video.videoId }]
|
||||
@@ -166,7 +169,7 @@
|
||||
class="wave thumbnail"
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
onclick={syncChangeVideo}
|
||||
onclick={onVideoSelected}
|
||||
>
|
||||
{#if !$interfaceLowBandwidthMode}
|
||||
{#if !thumbnail}
|
||||
@@ -213,6 +216,7 @@
|
||||
class="video-title"
|
||||
data-sveltekit-preload-data="off"
|
||||
href={watchUrl.toString()}
|
||||
onclick={onVideoSelected}
|
||||
>
|
||||
<span class="bold">{letterCase(video.title.trimEnd())}</span>
|
||||
</a>
|
||||
@@ -253,6 +257,7 @@
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.thumbnail img {
|
||||
|
||||
@@ -79,9 +79,11 @@ menu {
|
||||
@media screen and (max-width: 1000px) {
|
||||
menu.mobile {
|
||||
position: fixed !important;
|
||||
top: 10% !important;
|
||||
bottom: 0% !important;
|
||||
height: 100vh !important;
|
||||
width: 100% !important;
|
||||
z-index: 99999;
|
||||
transform: unset !important;
|
||||
}
|
||||
|
||||
main.root {
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
.shaka-text-container > div {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.player-theme {
|
||||
font-family: var(--font) !important;
|
||||
color: var(--on-surface) !important;
|
||||
}
|
||||
|
||||
.shaka-current-time {
|
||||
color: #fff !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.player-theme .shaka-video-container * {
|
||||
font-family: var(--font) !important;
|
||||
color: var(--on-surface) !important;
|
||||
}
|
||||
|
||||
.player-theme .shaka-overflow-menu,
|
||||
.player-theme .shaka-settings-menu {
|
||||
border-radius: 0.25rem;
|
||||
background: var(--surface-container-low);
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
z-index: 99999999 !important;
|
||||
pointer-events: auto !important;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.shaka-overflow-menu button,
|
||||
.shaka-settings-menu button {
|
||||
color: var(--text) !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.player-theme .shaka-end-time {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.player-theme .chapter-marker {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.player-theme .shaka-overflow-menu,
|
||||
.player-theme .shaka-settings-menu {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
position: fixed !important;
|
||||
top: 70% !important;
|
||||
left: 50% !important;
|
||||
height: 50vh !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
pointer-events: auto !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
padding: 1rem 0.5rem !important;
|
||||
box-shadow: var(--elevate2);
|
||||
background-color: var(--surface-container);
|
||||
z-index: 40;
|
||||
inset: auto auto 0 0;
|
||||
inline-size: 100%;
|
||||
max-block-size: 50vh;
|
||||
max-inline-size: none !important;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
color: var(--on-surface);
|
||||
line-height: normal;
|
||||
text-align: start;
|
||||
border-radius: 0.25rem;
|
||||
transition:
|
||||
all var(--speed2),
|
||||
0s background-color;
|
||||
}
|
||||
|
||||
.player-theme .shaka-overflow-menu::before,
|
||||
.player-theme .shaka-settings-menu::before {
|
||||
content: '' !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
background: transparent !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.player-theme .shaka-overflow-menu:hover,
|
||||
.player-theme .shaka-settings-menu:hover {
|
||||
pointer-events: auto !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#shaka-container {
|
||||
z-index: 98 !important;
|
||||
pointer-events: auto !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <=400px) {
|
||||
:deep(.shaka-text-container) {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.player-theme .shaka-overflow-menu,
|
||||
.player-theme .shaka-settings-menu {
|
||||
max-height: 80%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
@@ -205,5 +205,7 @@
|
||||
},
|
||||
"newest": "Neuste",
|
||||
"oldest": "Älteste",
|
||||
"popular": "Populär"
|
||||
"popular": "Populär",
|
||||
"favouriteChannel": "Kanal favorisieren",
|
||||
"unfavouriteChannel": "Kanal entfavorisieren"
|
||||
}
|
||||
|
||||
@@ -124,7 +124,17 @@
|
||||
"youtubeJsLoading": "Video failed to load, attempting to reload with local video fallback.",
|
||||
"retryText": "Video failed to load, how would you like to continue?",
|
||||
"enableYoutubejsTemp": "Use local video fallback once",
|
||||
"enableYoutubejsPerm": "Always allow local video fallback"
|
||||
"enableYoutubejsPerm": "Always allow local video fallback",
|
||||
"controls": {
|
||||
"back": "Back",
|
||||
"quality": "Quality",
|
||||
"auto": "Auto",
|
||||
"playbackSpeed": "Playback speed",
|
||||
"loop": "Loop",
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"language": "Language"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"interface": "Interface",
|
||||
|
||||
@@ -205,5 +205,7 @@
|
||||
},
|
||||
"newest": "Neusti",
|
||||
"oldest": "Ältisti",
|
||||
"popular": "Populär"
|
||||
"popular": "Populär",
|
||||
"favouriteChannel": "Kanal favorisiere",
|
||||
"unfavouriteChannel": "Kanal entfavorisiere"
|
||||
}
|
||||
|
||||
@@ -205,5 +205,7 @@
|
||||
},
|
||||
"newest": "Legújabb",
|
||||
"oldest": "Legrégebbi",
|
||||
"popular": "Népszerű"
|
||||
"popular": "Népszerű",
|
||||
"favouriteChannel": "Csatorna kedvelése",
|
||||
"unfavouriteChannel": "Csatorna eltávolítása a kedvencekből"
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@
|
||||
"materialiousLink": "Скопировать ссылку на Materialious",
|
||||
"invidiousRedirect": "Скопировать ссылку на сервер Invidious",
|
||||
"youtubeLink": "Скопировать ссылку на Youtube",
|
||||
"includeTimestamp": "С отметкой времени"
|
||||
"includeTimestamp": "С отметкой времени",
|
||||
"copiedSuccess": "Скопированно в буфер обмена"
|
||||
},
|
||||
"download": "Скачать",
|
||||
"downloadSteps": {
|
||||
@@ -176,7 +177,8 @@
|
||||
"companionUrl": "URL-адрес компаньона",
|
||||
"expandChapters": "Раскрывать главы по умолчанию",
|
||||
"disableAutoUpdate": "Отключить автоматические обновления",
|
||||
"about": "О приложении"
|
||||
"about": "О приложении",
|
||||
"androidNativeShare": "Использовать native share"
|
||||
},
|
||||
"subscribe": "Подписаться",
|
||||
"invalidInstance": "Проверьте URL-адрес. Если он правильный, то возможно сервер сейчас недоступен или не поддерживает сторонние клиенты.",
|
||||
@@ -203,5 +205,7 @@
|
||||
"contributors": "Участники",
|
||||
"newest": "Сначала новые",
|
||||
"oldest": "Сначала старые",
|
||||
"popular": "Популярные"
|
||||
"popular": "Популярные",
|
||||
"favouriteChannel": "Любимый канал",
|
||||
"unfavouriteChannel": "Нелюбимый канал"
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import shaka from 'shaka-player/dist/shaka-player.ui';
|
||||
|
||||
export class EndTimeElement extends shaka.ui.Element {
|
||||
public video: HTMLMediaElement;
|
||||
protected button: HTMLButtonElement;
|
||||
|
||||
constructor(parent: HTMLElement, controls: shaka.ui.Controls) {
|
||||
super(parent, controls);
|
||||
|
||||
this.video = (this.player as shaka.Player).getMediaElement() as HTMLMediaElement;
|
||||
this.button = document.createElement('button');
|
||||
this.button.classList.add('shaka-current-time', 'shaka-end-time');
|
||||
this.button.disabled = true;
|
||||
(this.parent as HTMLElement).appendChild(this.button);
|
||||
|
||||
this.updateTime = this.updateTime.bind(this);
|
||||
(this.eventManager as shaka.util.EventManager).listen(
|
||||
this.video,
|
||||
'timeupdate',
|
||||
this.updateTime
|
||||
);
|
||||
}
|
||||
|
||||
private updateTime(): void {
|
||||
const duration = this.video.duration;
|
||||
const currentTime = this.video.currentTime;
|
||||
|
||||
if (isNaN(duration) || isNaN(currentTime)) return;
|
||||
|
||||
const remainingTime: number = duration - currentTime;
|
||||
const end: Date = new Date(Date.now() + remainingTime * 1000);
|
||||
|
||||
const formatted: string = end.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
|
||||
this.button.textContent = `Ends at ${formatted}`;
|
||||
}
|
||||
}
|
||||
@@ -418,6 +418,7 @@
|
||||
<i>close</i>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="space"></div>
|
||||
<nav class="s">
|
||||
<a
|
||||
class="button border"
|
||||
@@ -432,7 +433,7 @@
|
||||
{/if}
|
||||
<div class="player">
|
||||
{#key $playerState.data.video.videoId}
|
||||
<Player data={$playerState.data} />
|
||||
<Player data={$playerState.data} hideControls={playerIsPip} />
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -390,6 +390,7 @@
|
||||
{premiereTime}
|
||||
</h6>
|
||||
</article>
|
||||
<div class="space"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -718,6 +719,10 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.video-actions {
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
</script>
|
||||
|
||||
{#key data.video.videoId}
|
||||
<Player bind:playerElement isEmbed={true} {data} />
|
||||
<Player bind:playerElement isEmbed={true} {data} hideControls={true} />
|
||||
{/key}
|
||||
|
||||
{#if showInfo}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.12.5"
|
||||
LATEST_VERSION = "1.12.6"
|
||||
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