@@ -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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.15.5",
|
||||
"version": "1.15.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run patch:github && vite dev",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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('/', {}));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -342,7 +342,7 @@ export const engineCooldownYTStore: Writable<number> = persist(
|
||||
'engineCooldownYT'
|
||||
);
|
||||
export const engineMaxConcurrentChannelsStore: Writable<number> = persist(
|
||||
writable(100),
|
||||
writable(50),
|
||||
createStorage(),
|
||||
'engineMaxConcurrentChannels'
|
||||
);
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user