Merge pull request #1440 from Materialious/update/1.15.6

Update/1.15.6
This commit is contained in:
Ward
2026-02-16 21:39:11 +13:00
committed by GitHub
20 changed files with 148 additions and 83 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 213
versionName "1.15.5"
versionCode 214
versionName "1.15.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -78,7 +78,11 @@
<release version="1.15.5" date="2026-2-16">
<release version="1.15.6" date="2026-2-16">
<url>https://github.com/Materialious/Materialious/releases/tag/1.15.6</url>
</release>
<release version="1.15.5" date="2026-2-16">
<url>https://github.com/Materialious/Materialious/releases/tag/1.15.5</url>
</release>
<release version="1.15.4" date="2026-2-16">
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.15.5",
"version": "1.15.6",
"description": "Modern material design for YouTube and Invidious.",
"author": {
"name": "Ward Pearce",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.15.5",
"version": "1.15.6",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
+9 -3
View File
@@ -85,16 +85,22 @@ export async function deleteUnsubscribeBackend(authorId: string) {
});
}
export async function postSubscribeBackend(authorId: string) {
export async function postSubscribeBackend(
authorId: string,
authorName: string | undefined = undefined
) {
const rawKey = await getRawKey();
if (!rawKey) return;
const internalAuthorId = await getInternalAuthorId(authorId, rawKey);
const channel = await getChannelYTjs(authorId);
if (!authorName) {
const channel = await getChannelYTjs(authorId);
authorName = channel.author;
}
const channelId = await encryptWithMasterKey(authorId);
const channelName = await encryptWithMasterKey(channel.author);
const channelName = await encryptWithMasterKey(authorName);
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
method: 'POST',
@@ -60,7 +60,9 @@ export async function deleteUnsubscribeYTjs(authorId: string) {
export async function parseChannelRSS(channelId: string): Promise<void> {
const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
const response = await fetch(feedUrl);
const response = await fetch(feedUrl, { priority: 'low' });
if (!response.ok) return;
const text = await response.text();
const parser = new DOMParser();
@@ -6,10 +6,11 @@
import ContentColumn from './ContentColumn.svelte';
import { onMount, onDestroy, tick } from 'svelte';
import Mousetrap from 'mousetrap';
import { extractUniqueId, type feedItems } from '$lib/misc';
import { extractUniqueId, timeout, type feedItems } from '$lib/misc';
import ChannelThumbnail from './ChannelThumbnail.svelte';
import PlaylistThumbnail from './PlaylistThumbnail.svelte';
import HashtagThumbnail from './HashtagThumbnail.svelte';
import NoResults from './NoResults.svelte';
interface Props {
items?: feedItems;
@@ -184,15 +185,16 @@
} else if ($feedLastItemId) {
const element = document.getElementById($feedLastItemId);
feedLastItemId.set(undefined);
if (element) {
await timeout(100);
element.scrollIntoView({
behavior: 'instant',
block: 'start',
inline: 'nearest'
});
}
feedLastItemId.set(undefined);
}
});
@@ -215,16 +217,7 @@
<div class={classes} class:android-container={$isAndroidTvStore}>
{#if items.length === 0}
<div class="space"></div>
<div class="center-align">
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
><path
style="fill: var(--secondary);"
d="M11 0h2v4h-2zM5.001.586L3.587 2L6.5 4.914L7.915 3.5zM1 13.734L5.42 6h13.16L23 13.734V22H1zM6.58 8l-2.857 5h5.178l.193.75a3.002 3.002 0 0 0 5.812 0l.193-.75h5.178L17.42 8zM18.999.586L16.085 3.5l1.414 1.414L20.413 2z"
/></svg
>
<h1>{$_('noResult')}</h1>
</div>
<NoResults />
{/if}
<div class="grid" bind:this={gridElement}>
{#each items as item, index (index)}
@@ -0,0 +1,14 @@
<script lang="ts">
import { _ } from '$lib/i18n';
</script>
<div class="space"></div>
<div class="center-align">
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
><path
style="fill: var(--secondary);"
d="M11 0h2v4h-2zM5.001.586L3.587 2L6.5 4.914L7.915 3.5zM1 13.734L5.42 6h13.16L23 13.734V22H1zM6.58 8l-2.857 5h5.178l.193.75a3.002 3.002 0 0 0 5.812 0l.193-.75h5.178L17.42 8zM18.999.586L16.085 3.5l1.414 1.414L20.413 2z"
/></svg
>
<h1>{$_('noResult')}</h1>
</div>
@@ -7,13 +7,16 @@
engineFallbacksStore,
engineMaxConcurrentChannelsStore,
invidiousInstanceStore,
backendInUseStore
backendInUseStore,
rawMasterKeyStore
} from '$lib/store';
import { useEngineFallback, type EngineFallback } from '$lib/api/misc';
import { get } from 'svelte/store';
import { getSubscriptions, postSubscribe } from '$lib/api';
import { postSubscribeYTjs } from '$lib/api/youtubejs/subscriptions';
import { addToast } from '../Toast.svelte';
import { isOwnBackend } from '$lib/shared';
import { postSubscribeBackend } from '$lib/api/backend';
const engineFallbacks: EngineFallback[] = [
'Channel',
@@ -53,9 +56,17 @@
}
});
let subscribeFunction: (id: string, name: string) => Promise<void>;
if (isOwnBackend()?.internalAuth && $rawMasterKeyStore) {
subscribeFunction = postSubscribeBackend;
} else {
subscribeFunction = postSubscribeYTjs;
}
const subPromises: Promise<void>[] = [];
importedSubs.forEach((sub) => {
subPromises.push(postSubscribeYTjs(sub.authorId, sub.author));
subPromises.push(subscribeFunction(sub.authorId, sub.author));
});
await Promise.all(subPromises);
@@ -11,7 +11,12 @@
import type { RgbaColor, HsvaColor, Colord } from 'colord';
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { isUnrestrictedPlatform, setInvidiousInstance, goToInvidiousLogin } from '../../misc';
import {
isUnrestrictedPlatform,
setInvidiousInstance,
goToInvidiousLogin,
invidiousLogout
} from '../../misc';
import { getPages, type Pages } from '../../navPages';
import ColorPicker from 'svelte-awesome-color-picker';
import {
@@ -158,12 +163,7 @@
<span>{$_('linkInvidious')}</span>
</button>
{:else}
<button
onclick={() => {
invidiousAuthStore.set(null);
goto(resolve('/', {}));
}}
>
<button onclick={invidiousLogout}>
<i>link_off</i>
<span>{$_('unlinkInvidious')}</span>
</button>
@@ -1,6 +1,6 @@
<script lang="ts">
import { _ } from '$lib/i18n';
import { logout } from '$lib/misc';
import { materialiousLogout } from '$lib/misc';
let clickCount = $state(0);
const clicksToDelte = 3;
@@ -16,7 +16,7 @@
if (clicksToDelte - clickCount === 0) {
await fetch('/api/user/delete', { method: 'DELETE' });
logout();
materialiousLogout();
}
}
</script>
@@ -5,6 +5,7 @@
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import {
backendInUseStore,
playerAlwaysLoopStore,
playerAndroidLockOrientation,
playerAndroidPauseOnNetworkChange,
@@ -22,7 +23,7 @@
playerYouTubeJsFallback
} from '../../store';
import { playbackRates } from '$lib/player';
import { isUnrestrictedPlatform, isYTBackend } from '$lib/misc';
import { isUnrestrictedPlatform } from '$lib/misc';
let defaultLanguage = $state(get(playerDefaultLanguage));
@@ -122,7 +123,7 @@
<i>arrow_drop_down</i>
</div>
{#if isUnrestrictedPlatform() && !isYTBackend()}
{#if isUnrestrictedPlatform() && $backendInUseStore === 'ivg'}
<div class="field suffix surface-container-highest label">
<select
tabindex="0"
+6 -3
View File
@@ -312,7 +312,12 @@ export async function goToInvidiousLogin() {
}
}
export async function logout() {
export async function invidiousLogout() {
invidiousAuthStore.set(null);
goto(resolve('/', {}));
}
export async function materialiousLogout() {
if (isYTBackend()) {
await clearFeedYTjs();
}
@@ -320,8 +325,6 @@ export async function logout() {
if (isOwnBackend()?.internalAuth) {
fetch('/api/user/logout', { method: 'DELETE' });
rawMasterKeyStore.set(undefined);
} else {
invidiousAuthStore.set(null);
}
goto(resolve('/', {}));
+3 -1
View File
@@ -24,7 +24,9 @@ export interface PlayerEvents {
events: PlayerEvent[];
}
export const playbackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3];
export const playbackRates = [
0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4
];
export const playerDoubleTapSeek = 10.0;
export function goToPreviousVideo(playlistId: string | null) {
+1 -1
View File
@@ -342,7 +342,7 @@ export const engineCooldownYTStore: Writable<number> = persist(
'engineCooldownYT'
);
export const engineMaxConcurrentChannelsStore: Writable<number> = persist(
writable(100),
writable(50),
createStorage(),
'engineMaxConcurrentChannels'
);
+14 -4
View File
@@ -32,7 +32,13 @@
import 'material-dynamic-colors';
import { onMount } from 'svelte';
import { _ } from '$lib/i18n';
import { goToInvidiousLogin, isYTBackend, logout, truncate } from '$lib/misc';
import {
goToInvidiousLogin,
invidiousLogout,
isYTBackend,
materialiousLogout,
truncate
} from '$lib/misc';
import Author from '$lib/components/Author.svelte';
import Toast from '$lib/components/Toast.svelte';
import { isOwnBackend } from '$lib/shared';
@@ -79,6 +85,10 @@
}
}
async function logout() {
await ($rawMasterKeyStore ? materialiousLogout : invidiousLogout)();
}
let loginError: boolean = $state(false);
let rawUsername: string = $state('');
let rawPassword: string = $state('');
@@ -141,15 +151,15 @@
onMount(async () => {
if ($invidiousAuthStore && !isYTBackend()) {
loadNotifications().catch(() => logout());
loadNotifications().catch(() => invidiousLogout());
}
if ($rawMasterKeyStore) {
fetch('/api/user/isLoggedIn', { method: 'GET', credentials: 'same-origin' })
.then((resp) => {
if (!resp.ok) logout();
if (!resp.ok) materialiousLogout();
})
.catch(logout);
.catch(materialiousLogout);
}
resetScroll();
@@ -3,6 +3,7 @@
import { deleteUnsubscribe } from '$lib/api';
import Fuse from 'fuse.js';
import { _ } from '$lib/i18n';
import NoResults from '$lib/components/NoResults.svelte';
let { data } = $props();
@@ -34,30 +35,35 @@
}
</script>
<div class="padding">
<div class="max field suffix prefix small no-margin surface-variant">
<i class="front">search</i><input
bind:value={search}
oninput={searchSubs}
type="text"
placeholder={$_('searchPlaceholder')}
/>
</div>
{#if subscriptions.length > 0}
<div class="padding">
<div class="max field suffix prefix small no-margin surface-variant">
<i class="front">search</i><input
bind:value={search}
oninput={searchSubs}
type="text"
placeholder={$_('searchPlaceholder')}
/>
</div>
{#each subscriptions as sub (sub.authorId)}
<article>
<nav>
<a href={resolve(`/channel/[authorId]`, { authorId: sub.authorId })}
><h6>
{sub.author}
</h6></a
>
<div class="max"></div>
<button onclick={async () => unsubscribe(sub.authorId)} class="border">Unsubscribe</button>
</nav>
</article>
{/each}
</div>
{#each subscriptions as sub (sub.authorId)}
<article>
<nav>
<a href={resolve(`/channel/[authorId]`, { authorId: sub.authorId })}
><h6>
{sub.author}
</h6></a
>
<div class="max"></div>
<button onclick={async () => unsubscribe(sub.authorId)} class="border">Unsubscribe</button
>
</nav>
</article>
{/each}
</div>
{:else}
<NoResults />
{/if}
<style>
@media screen and (max-width: 590px) {
@@ -32,13 +32,6 @@ for (const dynamicDomain of dynamicAllowDomainsEnvVars) {
}
}
function copyHeader(headerName: string, to: Headers, from: Headers) {
const hdrVal = from.get(headerName);
if (hdrVal) {
to.set(headerName, hdrVal);
}
}
async function proxyRequest(
request: Request,
urlToProxy: string,
@@ -80,13 +73,23 @@ async function proxyRequest(
}
}
const requestHeaders = new Headers();
const requestHeaders = new Headers(request.headers);
for (const key of [
'referer',
'x-forwarded-for',
'x-requested-with',
'sec-ch-ua-mobile',
'sec-ch-ua',
'sec-ch-ua-platform',
'content-length'
]) {
requestHeaders.delete(key);
}
requestHeaders.set('host', urlToProxyObj.host);
requestHeaders.set('origin', urlToProxyObj.origin);
requestHeaders.set('user-agent', USER_AGENT);
copyHeader('range', requestHeaders, request.headers);
const requestOptions: RequestInit = {
method: request.method,
headers: requestHeaders,
@@ -104,11 +107,21 @@ async function proxyRequest(
body = request.body;
}
const response = await fetch(urlToProxy, { ...requestOptions, body });
const responseBody = await response.blob();
const response = await fetch(urlToProxyObj.toString(), {
...requestOptions,
body,
signal: AbortSignal.timeout(10000)
});
return new Response(responseBody, {
status: response.status
const responseHeaders = new Headers(response.headers);
responseHeaders.set('transfer-encoding', 'chunked');
responseHeaders.delete('content-encoding');
responseHeaders.set('access-control-allow-origin', request.headers.get('origin') ?? '');
responseHeaders.set('timing-allow-origin', request.headers.get('origin') ?? '');
return new Response(response.body, {
status: response.status,
headers: responseHeaders
});
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.15.5"
LATEST_VERSION = "1.15.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")