Merge pull request #1197 from Materialious/update/1.11.7

Update/1.11.7
This commit is contained in:
Ward
2025-11-20 08:03:03 +00:00
committed by GitHub
39 changed files with 1055 additions and 862 deletions
-13
View File
@@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
-31
View File
@@ -1,31 +0,0 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 166
versionName "1.11.6"
versionCode 167
versionName "1.11.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -76,7 +76,11 @@
<release version="1.11.6" date="2025-11-13">
<release version="1.11.7" date="2025-11-20">
<url>https://github.com/Materialious/Materialious/releases/tag/1.11.7</url>
</release>
<release version="1.11.6" date="2025-11-13">
<url>https://github.com/Materialious/Materialious/releases/tag/1.11.6</url>
</release>
<release version="1.11.5" date="2025-11-08">
+46 -33
View File
@@ -1,17 +1,16 @@
{
"name": "Materialious",
"version": "1.11.6",
"version": "1.11.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.11.6",
"version": "1.11.7",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
"bgutils-js": "^3.2.0",
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
"chokidar": "~4.0.3",
"electron-is-dev": "~2.0.0",
"electron-serve": "~3.0.0",
@@ -841,6 +840,27 @@
"node": ">=16.0.0"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1486,6 +1506,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
@@ -1571,6 +1592,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -1863,15 +1885,6 @@
"node": ">= 0.4"
}
},
"node_modules/capacitor-nodejs": {
"version": "1.0.0-beta.9",
"resolved": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
"integrity": "sha512-4EU/QBolfuyHiMtwd1nxZ4wsK243tGE4VttKvVBG+x1yQuK9EIlEf3lbRjFXfjrJMTZNCqaP6glmdfj9Zo7KVg==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": ">=7.0.0"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2118,9 +2131,9 @@
}
},
"node_modules/config-file-ts/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3913,9 +3926,9 @@
}
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -4295,12 +4308,12 @@
}
},
"node_modules/minimatch": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"license": "ISC",
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^2.0.1"
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
@@ -5088,14 +5101,14 @@
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
"integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
"license": "ISC",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^4.0.1",
"minimatch": "^10.0.0",
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
@@ -5111,9 +5124,9 @@
}
},
"node_modules/rimraf/node_modules/jackspeak": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz",
"integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.11.6",
"version": "1.11.7",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
+70
View File
@@ -0,0 +1,70 @@
// based on eslint-plugin-svelte README.md
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
// See more details at: https://typescript-eslint.io/packages/parser/
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'], // Add support for additional file extensions, such as .svelte
parser: ts.parser,
sourceType: 'module',
ecmaVersion: 2020,
// Specify a parser for each language, if needed:
// parser: {
// ts: ts.parser,
// js: espree, // Use espree for .js files (add: import espree from 'espree')
// typescript: ts.parser
// },
// We recommend importing and specifying svelte.config.js.
// By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly.
// While certain Svelte settings may be statically loaded from svelte.config.js even if you dont specify it,
// explicitly specifying it ensures better compatibility and functionality.
//
// If non-serializable properties are included, running ESLint with the --cache flag will fail.
// In that case, please remove the non-serializable properties. (e.g. `svelteConfig: { ...svelteConfig, kit: { ...svelteConfig.kit, typescript: undefined }}`)
svelteConfig
}
}
},
{
rules: {
// Override or add rule settings here, such as:
// 'svelte/rule-name': 'error'
}
},
{
ignores: [
'.DS_Store',
'node_modules',
'/build',
'/.svelte-kit',
'/package',
'.env',
'.env.*',
'!.env.example',
// Ignore files for PNPM, NPM and YARN
'pnpm-lock.yaml',
'package-lock.json',
'yarn.lock'
],
}
);
+710 -662
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.11.6",
"version": "1.11.7",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -9,6 +9,7 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"eslint": "eslint .",
"format": "prettier --write .",
"patch:shaka": "node ./scripts/patchShaka.mjs",
"postinstall": "npm run patch:shaka"
@@ -20,15 +21,12 @@
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.46.5",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^8.56.7",
"@types/event-source-polyfill": "^1.0.5",
"@types/he": "^1.2.3",
"@types/human-number": "^1.0.2",
"@types/mousetrap": "^1.6.15",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.18.0",
"@vite-pwa/sveltekit": "^1.0.0",
"eslint": "^8.56.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.13.0",
"patch-package": "^8.0.1",
@@ -38,6 +36,7 @@
"svelte-check": "^4.3.3",
"tslib": "^2.7.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "^7.1.12"
},
"type": "module",
@@ -71,4 +70,4 @@
"svelte-persisted-store": "^0.12.0",
"youtubei.js": "^16.0.1"
}
}
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
import { cleanNumber } from '$lib/numbers';
import { onMount } from 'svelte';
@@ -28,7 +29,11 @@
});
</script>
<a href={`/channel/${channel.authorId}`} class="wave" style="min-width: 100%;min-height: 100%;">
<a
href={resolve(`/channel/[authorId]`, { authorId: channel.authorId })}
class="wave"
style="min-width: 100%;min-height: 100%;"
>
<div class="padding">
{#if !$interfaceLowBandwidthMode}
<div class="center-align">
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { cleanNumber } from '$lib/numbers';
import { _ } from '$lib/i18n';
import type { HashTag } from '../api/model';
@@ -11,7 +12,7 @@
let { hashtag }: Props = $props();
</script>
<a href={hashtag.url} class="wave" style="min-width: 100%;min-height: 100%;">
<a href={resolve(hashtag.url, {})} class="wave" style="min-width: 100%;min-height: 100%;">
<div class="padding">
<h5 class="center-align">{truncate(hashtag.title, 14)}</h5>
<h6 style="margin-top: 0;" class="center-align grey-text medium-text">
@@ -347,7 +347,7 @@
if (!import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE) {
captionUrl = caption.url.startsWith('http')
? caption.url
: `${get(instanceStore)}${caption.url}`;
: `${new URL(get(instanceStore)).origin}${caption.url}`;
} else {
captionUrl = `${import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE}${caption.url}`;
}
@@ -1,5 +1,6 @@
<script lang="ts">
import { getBestThumbnail } from '$lib/images';
import { resolve } from '$app/paths';
import { letterCase } from '$lib/letterCasing';
import { onMount } from 'svelte';
import { _ } from '$lib/i18n';
@@ -19,7 +20,7 @@
let img: HTMLImageElement | undefined = $state();
const playlistLink = `/playlist/${playlist.playlistId}`;
const playlistLink = resolve('/playlist/[playlistId]', { playlistId: playlist.playlistId });
onMount(async () => {
if (get(interfaceLowBandwidthMode)) return;
@@ -80,9 +81,13 @@
><div class="bold">{letterCase(truncate(playlist.title))}</div></a
>
<div>
<a class:link-disabled={playlist.authorId === null} href={`/channel/${playlist.authorId}`}
>{playlist.author}</a
>
{#if playlist.authorId}
<a href={resolve('/channel/[authorId]', { authorId: playlist.authorId })}
>{playlist.author}</a
>
{:else}
<p>{playlist.author}</p>
{/if}
</div>
</div>
</nav>
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { goto } from '$app/navigation';
import Mousetrap from 'mousetrap';
import { createEventDispatcher, onMount, tick } from 'svelte';
@@ -11,7 +12,6 @@
searchHistoryStore
} from '../store';
import { isVideoID } from '$lib/misc';
import { resolve } from '$app/paths';
const dispatch = createEventDispatcher();
@@ -52,7 +52,7 @@
}
selectedSuggestionIndex = -1;
goto(`/search/${encodeURIComponent(searchTrimed)}`);
goto(resolve(`/search/[search]`, { search: encodeURIComponent(searchTrimed) }));
suggestionsForSearch = [];
showSearchBox = false;
@@ -156,7 +156,7 @@
<i class="front" role="presentation" onclick={resetSearch}>close</i>
</div>
{#if searchSuggestions}
{#each suggestionsForSearch as suggestion, index}
{#each suggestionsForSearch as suggestion, index (index)}
<li>
<a
onclick={() => {
@@ -164,7 +164,7 @@
handleSubmit();
}}
class:selected={index === selectedSuggestionIndex}
href={`/search/${encodeURIComponent(suggestion)}`}
href={resolve(`/search/[search]`, { search: encodeURIComponent(suggestion) })}
>
<div>{suggestion}</div>
</a>
@@ -172,13 +172,13 @@
{/each}
{/if}
{#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled}
{#each $searchHistoryStore as history}
{#each $searchHistoryStore as history (history)}
<li>
<a
onclick={() => {
search = history;
}}
href={`/search/${encodeURIComponent(history)}`}
href={resolve(`/search/[search]`, { search: encodeURIComponent(history) })}
>
<div>{history}</div>
</a>
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { instanceStore } from '$lib/store';
import { Clipboard } from '@capacitor/clipboard';
import { Capacitor } from '@capacitor/core';
@@ -33,7 +34,7 @@
if (Capacitor.isNativePlatform()) {
url = `${get(instanceStore)}/watch/${video.videoId}`;
} else {
url = `${location.origin}/watch/${video.videoId}`;
url = `${location.origin}${resolve('/watch/[videoId]', { videoId: video.videoId })}`;
}
if (includeTimestamp && currentTime !== undefined) {
url += `?time=${Math.floor(currentTime)}`;
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import { peerJs, removeWindowQueryFlag, setWindowQueryFlag } from '$lib/misc';
import type { PlayerEvents } from '$lib/player';
@@ -20,7 +21,7 @@
events.events.forEach((event) => {
if (event.type === 'change-video' && event.videoId) {
currentUrl.pathname = `/watch/${event.videoId}`;
currentUrl.pathname = resolve(`/watch/[videoId]`, { videoId: event.videoId });
goto(currentUrl);
} else if (event.type === 'goto' && event.path && event.path !== $page.url.pathname) {
if (blockedPages.includes(event.path.replace('/', ''))) {
@@ -72,7 +73,7 @@
}
page.subscribe((newPage) => {
if (!newPage.url.pathname.startsWith('/watch') && $syncPartyPeerStore) {
if (!newPage.url.pathname.startsWith(resolve('/watch', {})) && $syncPartyPeerStore) {
$syncPartyConnectionsStore?.forEach((conn) => {
conn.send({
events: [
@@ -124,14 +125,16 @@
<input
name="sync-share"
readonly
value={`${location.origin}?sync=${$syncPartyPeerStore.id}`}
value={`${location.origin}${resolve(`/?sync=${$syncPartyPeerStore?.id}`, {})}`}
type="text"
/>
<label class="active" for="sync-share">Share URL</label>
</div>
<button
onclick={async () => {
await Clipboard.write({ string: `${location.origin}?sync=${$syncPartyPeerStore?.id}` });
await Clipboard.write({
string: `${location.origin}${resolve(`/?sync=${$syncPartyPeerStore?.id}`, {})}`
});
}}
class="square round"
>
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { getBestThumbnail } from '$lib/images';
import { letterCase } from '$lib/letterCasing';
import { videoLength } from '$lib/numbers';
@@ -212,7 +213,10 @@
</a>
<div>
<a tabindex="-1" class:author={!sideways} href={`/channel/${video.authorId}`}
<a
tabindex="-1"
class:author={!sideways}
href={resolve(`/channel/[authorId]`, { authorId: video.authorId })}
>{video.author}
</a>
@@ -48,7 +48,7 @@
isLoading = true;
transcript = null;
const resp = await fetch(`${!video.fallbackPatch ? get(instanceStore) : ''}${url}`);
const resp = await fetch(`${!video.fallbackPatch ? new URL(get(instanceStore)).origin : ''}${url}`);
transcript = await parseText(await resp.text(), { strict: false });
// Group cues by Math.ceil(startTime)
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { bookmarkletSaveToUrl } from '$lib/externalSettings';
import { letterCase, titleCases } from '$lib/letterCasing';
import { setAmoledTheme } from '$lib/theme';
@@ -89,7 +90,7 @@
instanceStore.set(instance);
authStore.set(null);
goto('/', { replaceState: true });
goto(resolve('/', {}), { replaceState: true });
ui('#dialog-settings');
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { deleteUnsubscribe, postSubscribe } from '$lib/api';
import type { VideoPlay } from '$lib/api/model';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
@@ -24,7 +25,7 @@
</script>
<nav>
<a href={`/channel/${video.authorId}`}>
<a href={resolve(`/channel/[authorId]`, { authorId: video.authorId })}>
<nav style="gap: 0.5em;">
{#if !$interfaceLowBandwidthMode}
<img
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { getComments } from '$lib/api';
import { type Comment, type Comments } from '$lib/api/model';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
@@ -34,7 +35,7 @@
function commentTimestamps(html: string): string {
const regex =
/<a href="([^"]+)" data-onclick="jump_to_time" data-jump-time="(\d+)">([^<]+)<\/a>\s*(.+)/g;
const replacement = `<a href="/watch/${videoId}?time=$2" data-sveltekit-preload-data="off" class="link">$3 $4</a>`;
const replacement = `<a href=${resolve(`/watch/[videoId]?time=$2`, { videoId: videoId })} data-sveltekit-preload-data="off" class="link">$3 $4</a>`;
const processedHtml = html.replace(regex, replacement);
@@ -63,7 +64,7 @@
<progress class="circle"></progress>
{/if}
<div class="comment-info">
<a href={`/channel/${comment.authorId}`} class="author">
<a href={resolve(`/channel/[authorId]`, { authorId: comment.authorId })} class="author">
<span class="bold" class:channel-owner={comment.authorIsChannelOwner}>
{comment.author}
</span>
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { cleanNumber } from '$lib/numbers';
import { _ } from '$lib/i18n';
import type { VideoPlay } from '$lib/api/model';
@@ -33,8 +34,11 @@
{#if video.keywords && video.keywords.length > 0}
<article class="border">
<nav class="scroll">
{#each video.keywords as keyword}
<a href={`/search/${encodeURIComponent(keyword)}`} class="chip">{keyword}</a>
{#each video.keywords as keyword (keyword)}
<a
href={resolve(`/search/[search]`, { search: encodeURIComponent(keyword) })}
class="chip">{keyword}</a
>
{/each}
</nav>
</article>
+18 -3
View File
@@ -147,7 +147,8 @@
"enableYoutubejsTemp": "Helyi videó visszatérés használata egyszer",
"enableYoutubejsPerm": "Mindig engedélyezze a helyi videó visszatérést",
"retryText": "A videó betöltése siketelen, hogyan szeretné folytatni?",
"youtubeJsLoading": "A videó betöltése sikertelen, újraprobálkozás a helyi videó visszatéréssel."
"youtubeJsLoading": "A videó betöltése sikertelen, újraprobálkozás a helyi videó visszatéréssel.",
"premiere": "A videó elérhető lesz ekkor"
},
"title": "Cím",
"subscriptions": {
@@ -163,7 +164,9 @@
"private": "Privát",
"playVideos": "Videók lejátszása",
"loopPlaylist": "Lejátszási lista folyamatos lejátszása",
"shuffleVideos": "Videók lejátszása keverten"
"shuffleVideos": "Videók lejátszása keverten",
"next": "Következő",
"previous": "Előző"
},
"thumbnail": {
"live": "ÉLŐ",
@@ -188,5 +191,17 @@
"subscribe": "Feliratkozás",
"invidiousBlockWarning": "Az Invidious jelenleg le van tiltva a Google által. Ha ebben az esetben a videók nem töltődnek be, akkor használja ezt a példányt a Materialious-on {android} vagy {desktop} platformon, hogy helyi videó visszatéréssel megkerülje ezt a problémát.",
"version": "Verzió",
"contributors": "Közreműködők"
"contributors": "Közreműködők",
"popularPageDisabled": "A népszerű oldal letiltva az adminok által",
"filters": {
"filters": "Szűrők",
"type": "Típus",
"uploadDate": "Feltöltés dátuma",
"duration": "Időtartam",
"sortBy": "Rendezés",
"features": "Jellemzők"
},
"newest": "Legújabb",
"oldest": "Legrégebbi",
"popular": "Népszerű"
}
+29 -13
View File
@@ -12,8 +12,8 @@
"delete": "Удалить",
"donate": "Пожертвовать",
"deleteAllHistory": "Удалить всю историю",
"skipping": "Пропускаю",
"replies": "ответ(а/ов)",
"skipping": "Пропуск",
"replies": "ответа(ов)",
"region": "Регион",
"noResult": "Без результатов",
"language": "Язык",
@@ -32,7 +32,7 @@
"videoTabs": {
"all": "Всё",
"videos": "Видео",
"playlists": "Списки воспроизведения",
"playlists": "Плейлисты",
"channels": "Каналы",
"shorts": "Shorts",
"streams": "Трансляции"
@@ -41,13 +41,15 @@
"manageSubscriptions": "Управление подписками"
},
"playlist": {
"createPlaylist": "Создать список воспроизведения",
"createPlaylist": "Создать плейлист",
"public": "Общедоступно",
"unlisted": "Недоступно",
"private": "Ограниченно",
"playVideos": "Воспроизвести видео",
"shuffleVideos": "Воспроизвести видео в случайном порядке",
"loopPlaylist": "Повторять список воспроизведения"
"loopPlaylist": "Зациклить плейлист",
"next": "Следующее",
"previous": "Предыдущее"
},
"thumbnail": {
"live": "В ЭФИРЕ",
@@ -64,7 +66,7 @@
"unsubscribe": "Отписаться",
"loginRequired": "Нужно войти в учётную запись",
"player": {
"audioOnly": "Только звук",
"audioOnly": "Только аудио",
"theatreMode": "Широкий экран",
"pauseTimer": "Таймер паузы",
"pauseVideoIn": "Приостановить видео на ",
@@ -86,7 +88,7 @@
"wasm": "Скачивание ffmpeg wasm",
"worker": "Скачивание ffmpeg worker"
},
"noPlaylists": "Нет списков воспроизведения",
"noPlaylists": "Нет плейлистов",
"unableToLoadComments": "Невозможно загрузить комментарии",
"addToPlaylist": "Добавить в плейлист",
"defaultLanguage": "Язык по умолчанию",
@@ -96,7 +98,8 @@
"youtubeJsLoading": "Не удалось скачать видео, попытка перезагрузки с локальным резервным видео.",
"chapters": "Главы",
"enableYoutubejsPerm": "Всегда использовать резервное локальное видео",
"preferredQuality": "Желаемое качество"
"preferredQuality": "Желаемое качество",
"premiere": "Видео будет доступно"
},
"layout": {
"interface": "Интерфейс",
@@ -129,10 +132,10 @@
"player": {
"title": "Плеер",
"autoPlay": "Авто-воспроизведение видео",
"alwaysLoopVideo": "Зацикливать видео",
"alwaysLoopVideo": "Всегда зацикливать видео",
"savePlaybackPosition": "Сохранять позицию",
"listenByDefault": "Слушать по умолчанию",
"theatreModeByDefault": "Широкий формат по умолчанию",
"theatreModeByDefault": "Широкий экран по умолчанию",
"autoPlayNextByDefault": "Авто-воспроизведение по умолчанию",
"miniPlayer": "Мини-плеер",
"proxyVideos": "Прокси видео",
@@ -153,7 +156,7 @@
"sponsor": "Спонсор",
"Catagories": "Категории",
"unpaidSelfPromotion": "Самореклама/рекомендация",
"interactionReminder": "Напоминание о взаимодействии (Подписка)",
"interactionReminder": "Напоминание о взаимодействии (Подписаться)",
"intermissionIntroAnimation": "Анимация концовки/вступления",
"credits": "Конечные заставки/титры",
"preViewRecapHook": "Предпросмотр/краткое содержание/отсылка",
@@ -186,6 +189,19 @@
"premium": "Премиум-контент YouTube нельзя просматривать в Materialious.",
"isUpcoming": "Видео ещё не готово к просмотру.",
"comments": "комментарии",
"reply": "ответить",
"version": "Версия"
"reply": "ответ",
"version": "Версия",
"popularPageDisabled": "Страница «Популярные» отключена администраторами",
"filters": {
"filters": "Фильтры",
"type": "Тип",
"uploadDate": "Дата загрузки",
"duration": "Длительность",
"sortBy": "Сортировать по",
"features": "Функции"
},
"contributors": "Участники",
"newest": "Сначала новые",
"oldest": "Сначала старые",
"popular": "Популярные"
}
+5 -3
View File
@@ -1,4 +1,5 @@
import { pushState } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import he from 'he';
import type Peer from 'peerjs';
@@ -128,9 +129,10 @@ export function expandSummery(id: string) {
}
export function createVideoUrl(videoId: string, playlistId: string): URL {
const watchUrl = new URL(
`${location.origin}/${get(isAndroidTvStore) ? 'tv' : 'watch'}/${videoId}`
);
const watchPath = resolve(`/${get(isAndroidTvStore) ? 'tv' : 'watch'}/[videoId]`, {
videoId: videoId
});
const watchUrl = new URL(`${location.origin}${watchPath}`);
if (playlistId !== '') {
watchUrl.searchParams.set('playlist', playlistId);
+32 -26
View File
@@ -10,9 +10,9 @@ import type {
} from '$lib/api/model';
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
import { Capacitor } from '@capacitor/core';
import BG, { USER_AGENT } from 'bgutils-js';
import { USER_AGENT } from 'bgutils-js';
import { get } from 'svelte/store';
import type { Types } from 'youtubei.js';
import type { Types, Misc } from 'youtubei.js';
import { Innertube, UniversalCache, Utils, YT, YTNodes, Platform } from 'youtubei.js';
Platform.shim.eval = async (
@@ -85,6 +85,29 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
let dashUri: string | undefined;
// Unsupported format fix
// https://github.com/LuanRT/googlevideo/issues/42
if (video.streaming_data)
video.streaming_data.adaptive_formats = video.streaming_data.adaptive_formats.filter(
(format) => format.xtags !== 'CgcKAnZiEgEx'
);
const adaptiveFormats: AdaptiveFormats[] = [];
video.streaming_data?.adaptive_formats.forEach((format) => {
adaptiveFormats.push({
index: format.index_range?.start?.toString() || '',
bitrate: format.bitrate?.toString() || '',
init: format.init_range?.start?.toString() || '',
url: format.url || '',
itag: format.itag?.toString() || '',
type: format.mime_type,
clen: '',
lmt: '',
projectionType: 0,
resolution: format.width ? `${format.width}x${format.height}` : undefined
});
});
const isPostLiveDVR =
!!video.basic_info.is_post_live_dvr ||
(video.basic_info.is_live_content &&
@@ -100,14 +123,13 @@ 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({
manifest_options: {
is_sabr: true,
include_thumbnails: false
}
})
)}`;
const manifest = await video.toDash({
manifest_options: {
is_sabr: true,
include_thumbnails: false
}
});
dashUri = `data:application/dash+xml;base64,${btoa(manifest)}`;
}
}
@@ -172,22 +194,6 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
});
});
const adaptiveFormats: AdaptiveFormats[] = [];
video.streaming_data?.adaptive_formats.forEach((format) => {
adaptiveFormats.push({
index: format.index_range?.start?.toString() || '',
bitrate: format.bitrate?.toString() || '',
init: format.init_range?.start?.toString() || '',
url: format.url || '',
itag: format.itag?.toString() || '',
type: format.mime_type,
clen: '',
lmt: '',
projectionType: 0,
resolution: format.width ? `${format.width}x${format.height}` : undefined
});
});
const storyboard: StoryBoard[] = [];
if (video.storyboards && 'boards' in video.storyboards) {
video.storyboards.boards.forEach((board) => {
+17 -7
View File
@@ -8,9 +8,9 @@ import {
syncPartyConnectionsStore
} from './store';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { loadEntirePlaylist } from './playlist';
import { unsafeRandomItem } from './misc';
import { resolve } from '$app/paths';
export interface PlayerEvent {
type: 'pause' | 'seek' | 'change-video' | 'play' | 'playlist' | 'goto';
@@ -44,9 +44,14 @@ export async function goToNextVideo(video: VideoPlay, playlistId: string | null)
if (!playlistId) {
if (get(playerAutoplayNextByDefaultStore)) {
goto(`/${isAndroidTv ? 'tv' : 'watch'}/${video.recommendedVideos[0].videoId}`, {
replaceState: isAndroidTv
});
goto(
resolve(`/${isAndroidTv ? 'tv' : 'watch'}/[videoId]`, {
videoId: video.recommendedVideos[0].videoId
}),
{
replaceState: isAndroidTv
}
);
}
return;
}
@@ -86,8 +91,13 @@ export async function goToNextVideo(video: VideoPlay, playlistId: string | null)
} as PlayerEvents);
});
goto(`/${isAndroidTv ? 'tv' : 'watch'}/${goToVideo.videoId}?playlist=${playlistId}`, {
replaceState: isAndroidTv
});
goto(
resolve(`/${isAndroidTv ? 'tv' : 'watch'}/[videoId]?playlist=${playlistId}`, {
videoId: goToVideo.videoId
}),
{
replaceState: isAndroidTv
}
);
}
}
+14 -9
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { goto } from '$app/navigation';
import { navigating, page } from '$app/stores';
@@ -102,7 +103,7 @@
return;
}
goto(`/watch/${videoId}`);
goto(resolve(`/watch/[videoId]`, { videoId: videoId }));
} else {
// Auth response handling for Mobile
const username = url.searchParams.get('username');
@@ -128,7 +129,7 @@
path.search = searchParams.toString();
await Browser.open({ url: path.toString() });
} else {
searchParams.set('callback_url', `${location.origin}/auth`);
searchParams.set('callback_url', `${location.origin}${resolve('/auth', {})}`);
path.search = searchParams.toString();
document.location.href = path.toString();
}
@@ -173,7 +174,7 @@
console.log(sid);
authStore.set({ username: rawUsername, token: sid });
await ui('#tv-login');
goto('/', { replaceState: true });
goto(resolve('/', {}), { replaceState: true });
return;
}
}
@@ -187,7 +188,7 @@
feedCacheStore.set({});
searchCacheStore.set({});
playlistCacheStore.set({});
goto('/');
goto(resolve('/', {}));
}
async function loadNotifications() {
@@ -277,7 +278,7 @@
<nav class="left m l surface-container" class:tv-nav={$isAndroidTvStore}>
<header
role="presentation"
onclick={() => goto($interfaceDefaultPage)}
onclick={() => goto(resolve($interfaceDefaultPage, {}))}
style="cursor: pointer;"
tabindex="-1"
class="small-padding"
@@ -286,7 +287,7 @@
</header>
{#each getPages() as navPage}
{#if !navPage.requiresAuth || isLoggedIn}
<a href={navPage.href} class:active={$page.url.href.endsWith(navPage.href)}
<a href={resolve(navPage.href, {})} class:active={$page.url.href.endsWith(navPage.href)}
><i>{navPage.icon}</i>
<div>{navPage.name}</div>
</a>
@@ -365,7 +366,7 @@
{#if !navPage.requiresAuth || isLoggedIn}
<a
class="round"
href={navPage.href}
href={resolve(navPage.href, {})}
class:active={$page.url.href.endsWith(navPage.href)}
data-sveltekit-preload-data="off"
><i>{navPage.icon}</i>
@@ -421,7 +422,9 @@
</div>
{#if playerIsPip}
<nav class="s">
<a class="button border" href={`/watch/${$playerState.data.video.videoId}`}
<a
class="button border"
href={resolve(`/watch/[videoId]`, { videoId: $playerState.data.video.videoId })}
><i>keyboard_arrow_right</i></a
>
<button class="border" onclick={() => playerState.set(undefined)}>
@@ -432,7 +435,9 @@
<nav class="m l">
<Author video={$playerState.data.video} hideSubscribe={true} />
<div class="max"></div>
<a class="button border" href={`/watch/${$playerState.data.video.videoId}`}
<a
class="button border"
href={resolve(`/watch/[videoId]`, { videoId: $playerState.data.video.videoId })}
><i>keyboard_arrow_right</i></a
>
</nav>
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import PageLoading from '$lib/components/PageLoading.svelte';
import { authStore } from '$lib/store';
@@ -17,7 +18,7 @@
});
}
goto('/');
goto(resolve('/', {}));
});
</script>
@@ -224,7 +224,7 @@
<div class="grid">
<div class="s12 m6 l6">
<nav class="group">
{#each sortByOptions as sortingOption}
{#each sortByOptions as sortingOption (sortingOption)}
<button
class="no-round"
onclick={async () => {
@@ -1,10 +1,11 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { error } from '@sveltejs/kit';
export async function load({ url }) {
const playlistId = url.searchParams.get('list');
if (playlistId) {
goto(`/playlist/${playlistId}`);
goto(resolve(`/playlist/[playlistId]`, { playlistId }));
} else {
error(404);
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { unsafeRandomItem } from '$lib/misc';
import { cleanNumber } from '$lib/numbers';
import { isAndroidTvStore, playlistSettingsStore } from '$lib/store';
@@ -14,9 +15,14 @@
{#if data.playlist.videos}
<nav>
<a
href={!$isAndroidTvStore
? `/watch/${data.playlist.videos[0].videoId}?playlist=${data.playlist.info.playlistId}`
: `/tv/${data.playlist.videos[0].videoId}?playlist=${data.playlist.info.playlistId}`}
href={resolve(
$isAndroidTvStore
? `/tv/[playlistId]?playlist=${data.playlist.info.playlistId}`
: `/watch/[playlistId]?playlist=${data.playlist.info.playlistId}`,
{
playlistId: data.playlist.videos[0].videoId
}
)}
class="button circle extra no-margin"
>
<i>play_arrow</i>
@@ -26,9 +32,14 @@
</a>
<a
href={!$isAndroidTvStore
? `/watch/${unsafeRandomItem(data.playlist.videos).videoId}?playlist=${data.playlist.info.playlistId}`
: `/tv/${unsafeRandomItem(data.playlist.videos).videoId}?playlist=${data.playlist.info.playlistId}`}
href={resolve(
$isAndroidTvStore
? `/tv/[playlistId]?playlist=${data.playlist.info.playlistId}`
: `/watch/[playlistId]?playlist=${data.playlist.info.playlistId}`,
{
playlistId: unsafeRandomItem(data.playlist.videos).videoId
}
)}
onclick={() =>
playlistSettingsStore.set({
[data.playlist.info.playlistId]: { shuffle: true, loop: false }
@@ -37,7 +37,7 @@
<div class="page right active">
<div class="space"></div>
<div class="grid">
{#each playlists.playlists as playlist}
{#each playlists.playlists as playlist (playlist.playlistId)}
<ContentColumn>
<article class="no-padding" style="height: 100%;">
{#key playlist.playlistId}
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { deleteUnsubscribe } from '$lib/api';
import Fuse from 'fuse.js';
import { _ } from '$lib/i18n';
@@ -42,10 +43,10 @@
/>
</div>
{#each subscriptions as sub}
{#each subscriptions as sub (sub.authorId)}
<article>
<nav>
<a href={`/channel/${sub.authorId}`}
<a href={resolve(`/channel/[authorId]`, { authorId: sub.authorId })}
><h6>
{sub.author}
</h6></a
+2 -1
View File
@@ -1,4 +1,5 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { error } from '@sveltejs/kit';
export async function load({ url }) {
@@ -7,7 +8,7 @@ export async function load({ url }) {
const timestamp = url.searchParams.get('t');
if (videoId) {
let goToUrl = new URL(`${location.origin}/watch/${videoId}`);
const goToUrl = new URL(`${location.origin}${resolve(`/watch/[videoId]`, { videoId })}`);
if (playlistId) {
goToUrl.searchParams.set('playlist', playlistId);
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import {
addPlaylistVideo,
getComments,
@@ -453,7 +454,7 @@
<i>add</i>
<div class="tooltip">{$_('player.addToPlaylist')}</div>
<menu class="no-wrap mobile">
{#each personalPlaylists as personalPlaylist}
{#each personalPlaylists as personalPlaylist (personalPlaylist.playlistId)}
<li
role="presentation"
class="row"
@@ -509,7 +510,7 @@
<div class="space"></div>
<div class="chapter-list" id="chapters">
<ul class="list">
{#each data.content.timestamps as timestamp}
{#each data.content.timestamps as timestamp (timestamp)}
<li
role="presentation"
onclick={() => {
@@ -550,7 +551,7 @@
<div class="space"></div>
<div class="medium scroll">
{#each comments.comments as comment}
{#each comments.comments as comment (comment)}
<Comment {comment} videoId={data.video.videoId}></Comment>
{/each}
{#if comments.continuation}
@@ -580,8 +581,10 @@
{$_('videos')}
</p>
<p>
<a href={`/channel/${$playlistCacheStore[data.playlistId].info.authorId}`}
>{$playlistCacheStore[data.playlistId].info.author}</a
<a
href={resolve(`/channel/[authorId]`, {
authorId: $playlistCacheStore[data.playlistId].info.authorId
})}>{$playlistCacheStore[data.playlistId].info.author}</a
>
</p>
<nav>
@@ -638,7 +641,7 @@
<div class="space"></div>
{#each $playlistCacheStore[data.playlistId].videos as playlistVideo}
{#each $playlistCacheStore[data.playlistId].videos as playlistVideo (playlistVideo.videoId)}
<article
class="no-padding border"
style="margin: .7em;"
@@ -656,7 +659,7 @@
{/each}
</article>
{:else if data.video.recommendedVideos}
{#each data.video.recommendedVideos as recommendedVideo}
{#each data.video.recommendedVideos as recommendedVideo (recommendedVideo.videoId)}
<article class="no-padding border">
{#key recommendedVideo.videoId}
<Thumbnail video={recommendedVideo} sideways={true} />
+7 -5
View File
@@ -1,5 +1,6 @@
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import androidTv from '$lib/android/plugins/androidTv';
import { getResolveUrl } from '$lib/api';
import '$lib/i18n'; // Import to initialize. Important :)
@@ -17,13 +18,14 @@ export async function load({ url }) {
isAndroidTvStore.set((await androidTv.isAndroidTv()).value);
if (url.pathname.startsWith('/@')) {
const username = url.pathname.split('/')[1];
const resolvedRoot = resolve('/', {});
if (url.pathname.startsWith(resolvedRoot + '@')) {
const username = url.pathname.substring(resolvedRoot.length).split('/')[0];
try {
const resolvedUrl = await getResolveUrl(`www.youtube.com/${username}`);
if (resolvedUrl.pageType === 'WEB_PAGE_TYPE_CHANNEL') {
goto(`/channel/${resolvedUrl.ucid}`);
goto(resolve(`/channel/[authorId]`, { authorId: resolvedUrl.ucid }));
}
} catch {}
}
@@ -34,12 +36,12 @@ export async function load({ url }) {
defaultPage &&
defaultPage !== '/' &&
defaultPage.startsWith('/') &&
url.pathname === '/' &&
url.pathname === resolvedRoot &&
window.history.length < 3
) {
getPages().forEach((page) => {
if (page.href === defaultPage && (!page.requiresAuth || get(authStore))) {
goto(defaultPage);
goto(resolve(defaultPage, {}));
}
});
}
+3
View File
@@ -7,6 +7,9 @@ const config = {
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// Optional: Set base path for hosted Materialious here
//paths: {base: '/materialious'},
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.11.6"
LATEST_VERSION = "1.11.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")