Merge pull request #1002 from Materialious/update/1.9.18
Improved player position resuming and fix chapters on TV
This commit is contained in:
@@ -5,8 +5,8 @@ on:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "fastline/**"
|
||||
- "materialious/src/i18n/locales/**"
|
||||
- "./fastline/**"
|
||||
- "./materialious/src/lib/i18n/locales/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ on:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "fastline/**"
|
||||
- "materialious/src/i18n/locales/**"
|
||||
- "./fastline/**"
|
||||
- "./materialious/src/lib/i18n/locales/**"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ on:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "fastline/**"
|
||||
- "materialious/src/i18n/locales/**"
|
||||
- "./fastline/**"
|
||||
- "./materialious/src/lib/i18n/locales/**"
|
||||
|
||||
jobs:
|
||||
web-build:
|
||||
|
||||
@@ -234,13 +234,13 @@ Help [translate Materialious via Weblate](https://toolate.othing.xyz/projects/ma
|
||||
|
||||
# Special thanks to
|
||||
- [SecularSteve](https://github.com/SecularSteve) for creating Materialious' logo.
|
||||
- [Invidious](https://github.com/iv-org)
|
||||
- [Clipious](https://github.com/lamarios/clipious) for inspiration & a good source for learning more about undocumented Invidious routes.
|
||||
- [Invidious](https://github.com/iv-org).
|
||||
- [Clipious](https://github.com/lamarios/clipious) for inspiration & references to undocumented Invidious routes.
|
||||
- [Beer CSS](https://github.com/beercss/beercss) (Especially the [YouTube template](https://github.com/beercss/beercss/tree/main/src/youtube) what was used as the base for Materialious.)
|
||||
- Every dependency in [package.json](/materialious/package.json).
|
||||
- [LuanRT](https://github.com/LuanRT) for YouTube.js & SABR implementation.
|
||||
- [FreeTube](https://github.com/FreeTubeApp/FreeTube) for their subtitle fix.
|
||||
- [spotube](https://github.com/KRTirtho/spotube) for using their README layout for local installation.
|
||||
- Every dependency in [package.json](/materialious/package.json).
|
||||
|
||||
# Developers
|
||||
- [How Materialious is built](./docs/BUILDING.md)
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 134
|
||||
versionName "1.9.17"
|
||||
versionCode 135
|
||||
versionName "1.9.18"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -69,7 +69,11 @@
|
||||
|
||||
|
||||
|
||||
<release version="1.9.17" date="2025-6-25">
|
||||
|
||||
<release version="1.9.18" date="2025-6-25">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.18</url>
|
||||
</release>
|
||||
<release version="1.9.17" date="2025-6-25">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.17</url>
|
||||
</release>
|
||||
<release version="1.9.16" date="2025-6-24">
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.9.17",
|
||||
"version": "1.9.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.9.17",
|
||||
"version": "1.9.18",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.9.17",
|
||||
"version": "1.9.18",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
@@ -44,4 +44,4 @@
|
||||
"capacitor",
|
||||
"electron"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.9.17",
|
||||
"version": "1.9.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -420,13 +420,18 @@ export async function getThumbnail(
|
||||
return URL.createObjectURL(await resp.blob());
|
||||
}
|
||||
|
||||
function buildApiExtendedAuthHeaders(): Record<string, Record<string, string>> {
|
||||
const authToken = get(authStore)?.token ?? '';
|
||||
return { headers: { Authorization: `Bearer ${authToken.replace('SID=', '')}` } };
|
||||
}
|
||||
|
||||
export async function getVideoProgress(
|
||||
videoId: string,
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<SynciousProgressModel[]> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`, {
|
||||
...buildAuthHeaders(),
|
||||
...buildApiExtendedAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
@@ -439,7 +444,7 @@ export async function saveVideoProgress(
|
||||
time: number,
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
const headers: Record<string, Record<string, string>> = buildAuthHeaders();
|
||||
const headers: Record<string, Record<string, string>> = buildApiExtendedAuthHeaders();
|
||||
headers['headers']['Content-type'] = 'application/json';
|
||||
|
||||
await fetchErrorHandle(
|
||||
@@ -458,7 +463,7 @@ export async function deleteVideoProgress(videoId: string, fetchOptions: Request
|
||||
await fetchErrorHandle(
|
||||
await fetch(`${get(synciousInstanceStore)}/video/${videoId}`, {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders(),
|
||||
...buildApiExtendedAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
@@ -468,7 +473,7 @@ export async function deleteAllVideoProgress(fetchOptions: RequestInit = {}) {
|
||||
await fetchErrorHandle(
|
||||
await fetch(`${get(synciousInstanceStore)}/videos`, {
|
||||
method: 'DELETE',
|
||||
...buildAuthHeaders(),
|
||||
...buildApiExtendedAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
playerAndroidLockOrientation,
|
||||
playerAutoplayNextByDefaultStore,
|
||||
playerAutoPlayStore,
|
||||
playerCCByDefault,
|
||||
playerDefaultLanguage,
|
||||
playerDefaultPlaybackSpeed,
|
||||
playerDefaultQualityStore,
|
||||
@@ -313,6 +314,24 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
function toggleSubtitles() {
|
||||
const isVisible = player.isTextTrackVisible();
|
||||
if (isVisible) {
|
||||
player.setTextTrackVisibility(false);
|
||||
} else {
|
||||
const defaultLanguage = get(playerDefaultLanguage);
|
||||
const langCode = ISO6391.getCode(defaultLanguage);
|
||||
|
||||
const tracks = player.getTextTracks();
|
||||
const subtitleTrack = tracks.find((track) => track.language === langCode);
|
||||
|
||||
if (subtitleTrack) {
|
||||
player.selectTextTrack(subtitleTrack);
|
||||
player.setTextTrackVisibility(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate));
|
||||
|
||||
async function loadVideo() {
|
||||
@@ -359,6 +378,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (watchProgressTimeout) {
|
||||
clearInterval(watchProgressTimeout);
|
||||
}
|
||||
// Auto save watch progress every minute.
|
||||
watchProgressTimeout = setInterval(() => savePlayerPos(), 60000);
|
||||
setupSponsorSkip();
|
||||
@@ -378,9 +400,7 @@
|
||||
dashUrl += '?local=true';
|
||||
}
|
||||
|
||||
await player.load(dashUrl);
|
||||
|
||||
await loadPlayerPos();
|
||||
await player.load(dashUrl, await getLastPlayPos());
|
||||
} else {
|
||||
if (data.video.fallbackPatch === 'youtubejs') {
|
||||
await player.load(data.video.dashUrl);
|
||||
@@ -396,6 +416,9 @@
|
||||
|
||||
restoreQualityPreference();
|
||||
restoreDefaultLanguage();
|
||||
if ($playerCCByDefault) {
|
||||
toggleSubtitles();
|
||||
}
|
||||
|
||||
if ($playerDefaultPlaybackSpeed && playerElement) {
|
||||
playerElement.playbackRate = $playerDefaultPlaybackSpeed;
|
||||
@@ -541,21 +564,7 @@
|
||||
}
|
||||
|
||||
Mousetrap.bind('c', () => {
|
||||
const isVisible = player.isTextTrackVisible();
|
||||
if (isVisible) {
|
||||
player.setTextTrackVisibility(false);
|
||||
} else {
|
||||
const defaultLanguage = get(playerDefaultLanguage);
|
||||
const langCode = ISO6391.getCode(defaultLanguage);
|
||||
|
||||
const tracks = player.getTextTracks();
|
||||
const subtitleTrack = tracks.find((track) => track.language === langCode);
|
||||
|
||||
if (subtitleTrack) {
|
||||
player.selectTextTrack(subtitleTrack);
|
||||
player.setTextTrackVisibility(true);
|
||||
}
|
||||
}
|
||||
toggleSubtitles();
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -582,6 +591,10 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
playerElement.addEventListener('pause', async () => {
|
||||
savePlayerPos();
|
||||
});
|
||||
|
||||
playerElement.addEventListener('ended', async () => {
|
||||
if (!data.playlistId) {
|
||||
if ($playerAutoplayNextByDefaultStore) {
|
||||
@@ -650,8 +663,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPlayerPos() {
|
||||
if (loadTimeFromUrl($page)) return;
|
||||
async function getLastPlayPos(): Promise<number> {
|
||||
if (loadTimeFromUrl($page)) return 0;
|
||||
|
||||
let toSetTime = 0;
|
||||
|
||||
@@ -670,7 +683,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (toSetTime > 0 && playerElement) playerElement.currentTime = toSetTime;
|
||||
return toSetTime;
|
||||
}
|
||||
|
||||
function savePlayerPos() {
|
||||
@@ -713,16 +726,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
savePlayerPos();
|
||||
} catch (error) {}
|
||||
|
||||
Mousetrap.unbind(['left', 'right', 'space', 'c', 'f', 'shift+left', 'shift+right']);
|
||||
|
||||
if (watchProgressTimeout) {
|
||||
clearTimeout(watchProgressTimeout);
|
||||
}
|
||||
|
||||
try {
|
||||
savePlayerPos();
|
||||
} catch (error) {}
|
||||
|
||||
HttpFetchPlugin.cacheManager.clearCache();
|
||||
|
||||
if (playerElementResizeObserver) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
playerAndroidLockOrientation,
|
||||
playerAutoPlayStore,
|
||||
playerAutoplayNextByDefaultStore,
|
||||
playerCCByDefault,
|
||||
playerDefaultLanguage,
|
||||
playerDefaultPlaybackSpeed,
|
||||
playerDefaultQualityStore,
|
||||
@@ -133,6 +134,22 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.player.ccByDefault')}</div>
|
||||
</div>
|
||||
<label class="switch" tabindex="0" role="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$playerCCByDefault}
|
||||
onclick={() => playerCCByDefault.set(!$playerCCByDefault)}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
|
||||
@@ -34,7 +34,8 @@ import {
|
||||
themeColorStore,
|
||||
interfaceAutoExpandChapters,
|
||||
playerDefaultPlaybackSpeed,
|
||||
playerStatisticsByDefault
|
||||
playerStatisticsByDefault,
|
||||
playerCCByDefault
|
||||
} from '$lib/store';
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
|
||||
@@ -217,6 +218,11 @@ const persistedStores: {
|
||||
name: 'playerStatistics',
|
||||
store: playerStatisticsByDefault,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'CCByDefault',
|
||||
store: playerCCByDefault,
|
||||
type: 'boolean'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -227,7 +233,7 @@ function setStores(toSet: Record<string, any>) {
|
||||
userOverwritten = localStorage.getItem(store.name) !== null;
|
||||
} catch {}
|
||||
|
||||
let paramValue = toSet[store.name];
|
||||
const paramValue = toSet[store.name];
|
||||
if (typeof paramValue !== 'undefined' && !userOverwritten) {
|
||||
let value: any;
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
"alwaysLoopVideo": "Always loop video",
|
||||
"savePlaybackPosition": "Save playback position",
|
||||
"listenByDefault": "Listen by default",
|
||||
"ccByDefault": "Subtitles on by default",
|
||||
"theatreModeByDefault": "Theatre mode by default",
|
||||
"autoPlayNextByDefault": "Autoplay next by default",
|
||||
"miniPlayer": "Mini player",
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
? video.streaming_data.hls_manifest_url // HLS is preferred for DVR streams.
|
||||
: `${video.streaming_data.dash_manifest_url}/mpd_version/7`;
|
||||
} else {
|
||||
dashUri = `data:application/dash+xml;base64,${btoa(await video.toDash(undefined))}`;
|
||||
dashUri = `data:application/dash+xml;base64,${btoa(await video.toDash({ captions_format: 'vtt' }))}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,16 +133,6 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
});
|
||||
});
|
||||
|
||||
const captions: Captions[] = [];
|
||||
video.captions?.caption_tracks?.forEach((caption) => {
|
||||
captions.push({
|
||||
label: caption.name?.toString() || '',
|
||||
language_code: caption.language_code,
|
||||
// Add correct format to url.
|
||||
url: caption.base_url + '&fmt=vtt'
|
||||
});
|
||||
});
|
||||
|
||||
const adaptiveFormats: AdaptiveFormats[] = [];
|
||||
video.streaming_data?.adaptive_formats.forEach((format) => {
|
||||
adaptiveFormats.push({
|
||||
@@ -194,7 +184,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
formatStreams: [],
|
||||
recommendedVideos: recommendedVideos,
|
||||
authorThumbnails: authorThumbnails,
|
||||
captions: captions,
|
||||
captions: [],
|
||||
authorId: video.basic_info.channel_id || '',
|
||||
authorUrl: `/channel/${video.basic_info.channel_id}`,
|
||||
authorVerified: false,
|
||||
|
||||
@@ -53,7 +53,8 @@ export const playerAutoplayNextByDefaultStore = persisted('autoplayNextByDefault
|
||||
export const playerYouTubeJsFallback = persisted('youTubeJsFallback', true);
|
||||
export const playerYouTubeJsAlways = persisted('youTubeJsAlways', false);
|
||||
export const playerAndroidLockOrientation = persisted('androidLockOrientation', true);
|
||||
export const playerDefaultLanguage = persisted('defaultLanguage', '');
|
||||
export const playerDefaultLanguage = persisted('defaultLanguage', 'english');
|
||||
export const playerCCByDefault = persisted('CCByDefault', false);
|
||||
export const playerDefaultPlaybackSpeed: Writable<number> = persisted('defaultPlaybackSpeed', 1);
|
||||
export const playerStatisticsByDefault = persisted('playerStatistics', false);
|
||||
|
||||
|
||||
@@ -111,22 +111,25 @@
|
||||
<h5 style="margin-bottom: 0;">{$_('player.chapters')}</h5>
|
||||
<div class="grid">
|
||||
{#each data.content.timestamps as timestamp}
|
||||
<article
|
||||
role="presentation"
|
||||
style="cursor: pointer;"
|
||||
onclick={() => {
|
||||
if (playerElement) playerElement.currentTime = timestamp.time;
|
||||
}}
|
||||
>
|
||||
<div style="white-space: pre-line; overflow-wrap: break-word;text-align: center;">
|
||||
<p style="no-margin no-padding">{timestamp.title}</p>
|
||||
<span
|
||||
class:primary={playerCurrentTime >= timestamp.time &&
|
||||
(playerCurrentTime <= timestamp.endTime || timestamp.endTime === -1)}
|
||||
class="chip no-margin">{timestamp.timePretty}</span
|
||||
>
|
||||
</div>
|
||||
</article>
|
||||
<ContentColumn>
|
||||
<article
|
||||
role="presentation"
|
||||
style="cursor: pointer;height: 100%;"
|
||||
onclick={() => {
|
||||
if (playerElement) playerElement.currentTime = timestamp.time;
|
||||
showInfo = false;
|
||||
}}
|
||||
>
|
||||
<div style="white-space: pre-line; overflow-wrap: break-word;text-align: center;">
|
||||
<p style="no-margin no-padding">{timestamp.title}</p>
|
||||
<span
|
||||
class:primary={playerCurrentTime >= timestamp.time &&
|
||||
(playerCurrentTime <= timestamp.endTime || timestamp.endTime === -1)}
|
||||
class="chip no-margin">{timestamp.timePretty}</span
|
||||
>
|
||||
</div>
|
||||
</article>
|
||||
</ContentColumn>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.9.17"
|
||||
LATEST_VERSION = "1.9.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