Merge pull request #1433 from Materialious/update/1.15.4

Update/1.15.4
This commit is contained in:
Ward
2026-02-16 03:43:28 +13:00
committed by GitHub
20 changed files with 102 additions and 86 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 211
versionName "1.15.3"
versionCode 212
versionName "1.15.4"
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.15.3" date="2026-2-15">
<release version="1.15.4" date="2026-2-16">
<url>https://github.com/Materialious/Materialious/releases/tag/1.15.4</url>
</release>
<release version="1.15.3" date="2026-2-15">
<url>https://github.com/Materialious/Materialious/releases/tag/1.15.3</url>
</release>
<release version="1.15.1" date="2026-2-15">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.15.3",
"version": "1.15.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.15.3",
"version": "1.15.4",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.15.3",
"version": "1.15.4",
"description": "Modern material design for YouTube and Invidious.",
"author": {
"name": "Ward Pearce",
+3 -15
View File
@@ -1,12 +1,12 @@
{
"name": "materialious",
"version": "1.15.3",
"version": "1.15.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
"version": "1.15.3",
"version": "1.15.4",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
@@ -50,13 +50,13 @@
"peerjs": "^1.5.5",
"pg": "^8.18.0",
"pg-hstore": "^2.3.4",
"psl": "^1.15.0",
"sequelize": "^6.37.7",
"shaka-player": "^4.16.14",
"sponsorblock-api": "^0.2.4",
"sqlite3": "^5.1.7",
"svelte-awesome-color-picker": "^4.1.1",
"svelte-infinite-loading": "^1.4.0",
"tldts": "^7.0.23",
"youtubei.js": "^16.0.1",
"zod": "^4.3.6"
},
@@ -12564,18 +12564,6 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.15.3",
"version": "1.15.4",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
@@ -86,14 +86,14 @@
"peerjs": "^1.5.5",
"pg": "^8.18.0",
"pg-hstore": "^2.3.4",
"psl": "^1.15.0",
"sequelize": "^6.37.7",
"shaka-player": "^4.16.14",
"sponsorblock-api": "^0.2.4",
"sqlite3": "^5.1.7",
"svelte-awesome-color-picker": "^4.1.1",
"svelte-infinite-loading": "^1.4.0",
"tldts": "^7.0.23",
"youtubei.js": "^16.0.1",
"zod": "^4.3.6"
}
}
}
@@ -52,7 +52,7 @@
{#if contributor.type === 'User'}
<div class="s12 m4 l4">
<article>
<img class="circle" src={contributor.avatar_url} alt={contributor.login} />
<img class="circle" loading="lazy" src={contributor.avatar_url} alt={contributor.login} />
<p>{contributor.login}</p>
</article>
</div>
@@ -1,5 +1,4 @@
<script lang="ts">
import { isYTBackend } from '$lib/misc';
import { _ } from '$lib/i18n';
import {
invidiousAuthStore,
@@ -7,7 +6,8 @@
engineCullYTStore,
engineFallbacksStore,
engineMaxConcurrentChannelsStore,
invidiousInstanceStore
invidiousInstanceStore,
backendInUseStore
} from '$lib/store';
import { useEngineFallback, type EngineFallback } from '$lib/api/misc';
import { get } from 'svelte/store';
@@ -95,7 +95,7 @@
<p>{$_('layout.backendEngine.warning')}</p>
</article>
{#if isYTBackend()}
{#if $backendInUseStore === 'yt'}
<h6>Feed</h6>
<div class="field label prefix surface-container-highest">
<i>view_stream</i>
@@ -11,12 +11,7 @@
import type { RgbaColor, HsvaColor, Colord } from 'colord';
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import {
isMobile,
isUnrestrictedPlatform,
setInvidiousInstance,
goToInvidiousLogin
} from '../../misc';
import { isUnrestrictedPlatform, setInvidiousInstance, goToInvidiousLogin } from '../../misc';
import { getPages, type Pages } from '../../navPages';
import ColorPicker from 'svelte-awesome-color-picker';
import {
@@ -179,10 +174,6 @@
<div class="space"></div>
{/if}
{#if isMobile() && invalidInstance}
<div style="margin-bottom: 6em;"></div>
{/if}
{#if Capacitor.isNativePlatform() && (invalidInstance || $interfaceAllowInsecureRequests)}
<div class="field no-margin">
<nav class="no-padding">
@@ -17,6 +17,8 @@
let activeTab = $state('interface');
const isActive = (id: string) => activeTab === id;
let mobileCategoriesButton: HTMLElement | undefined = $state();
let tabs: { id: string; label: string; icon: string; component: Component }[] = $state([
{ id: 'interface', label: $_('layout.interface'), icon: 'grid_view', component: Interface },
{ id: 'player', label: $_('layout.player.title'), icon: 'smart_display', component: Player },
@@ -121,17 +123,21 @@
{@const currentTab = tabs.find((tab) => tab.id === activeTab)}
{#if currentTab}
<button class="large small-round surface-container-highest max" data-ui="#tab-menu">
<button
bind:this={mobileCategoriesButton}
class="large small-round surface-container-highest max"
data-ui="#tab-menu"
>
<i>{currentTab.icon}</i>
<span>{currentTab.label}</span>
<menu style="width: 100%;" data-ui="#tab-menu" id="tab-menu">
{#each tabs as tab (tab)}
<li
role="presentation"
onclick={() => {
activeTab = tab.id;
mobileCategoriesButton?.click();
}}
role="presentation"
data-ui="#tab-menu"
>
<i>{tab.icon}</i>
<span>{tab.label}</span>
@@ -25,6 +25,8 @@ export async function syncSettingsToBackend() {
let initalLoad = true;
store.store.subscribe((value) => {
if (!get(rawMasterKeyStore)) return;
if (initalLoad) {
initalLoad = false;
return;
@@ -105,11 +107,11 @@ export function loadSettingsFromEnv() {
export function bookmarkletSaveToUrl(): string {
const url = new URL(location.origin);
for (const { name, store, serialize } of persistedStores) {
for (const { name, store, serialize, excludeFromBookmarklet } of persistedStores) {
const value = get(store);
const encoded = serialize ? serialize(value) : value?.toString();
if (encoded !== undefined) {
if (encoded !== undefined && !excludeFromBookmarklet) {
url.searchParams.set(name, encoded);
}
}
@@ -61,6 +61,7 @@ type PersistedStore<T> = {
store: Writable<T>;
schema: z.ZodType<T>;
serialize?: (value: T) => string;
excludeFromBookmarklet?: boolean; // Won't be include in bookmarklet
};
const zBoolean = z.coerce.boolean();
@@ -269,7 +270,8 @@ if (isOwnBackend()) {
name: 'authToken',
store: invidiousAuthStore,
schema: zAuth,
serialize: JSON.stringify
serialize: JSON.stringify,
excludeFromBookmarklet: true
});
persistedStores.push({
name: 'backendInUse',
@@ -304,27 +306,32 @@ if (isOwnBackend()) {
persistedStores.push({
name: 'allowInsecureRequests',
store: interfaceAllowInsecureRequests,
schema: zString
schema: zString,
excludeFromBookmarklet: true
});
persistedStores.push({
name: 'disableAutoUpdate',
store: interfaceDisableAutoUpdate,
schema: zString
schema: zString,
excludeFromBookmarklet: true
});
persistedStores.push({
name: 'androidUseNativeShare',
store: interfaceAndroidUseNativeShare,
schema: zString
schema: zString,
excludeFromBookmarklet: true
});
persistedStores.push({
name: 'pauseOnNetworkChange',
store: playerAndroidPauseOnNetworkChange,
schema: zBoolean
schema: zBoolean,
excludeFromBookmarklet: true
});
persistedStores.push({
name: 'androidLockOrientation',
store: playerAndroidLockOrientation,
schema: zBoolean
schema: zBoolean,
excludeFromBookmarklet: true
});
persistedStores.push({
name: 'youTubeJsFallback',
+1
View File
@@ -368,6 +368,7 @@ export const searchHistoryStore: Writable<string[]> = persist(
'searchHistory'
);
export const feedLoadingStore: Writable<boolean> = writable(false);
export const feedCacheStore: Writable<{
[key: string]: (VideoBase | Video | PlaylistPageVideo)[];
}> = writable({});
+5 -1
View File
@@ -23,7 +23,8 @@
playertheatreModeIsActive,
rawMasterKeyStore,
syncPartyPeerStore,
themeColorStore
themeColorStore,
backendInUseStore
} from '$lib/store';
import { Capacitor } from '@capacitor/core';
import 'beercss';
@@ -51,6 +52,9 @@
rawMasterKeyStore.subscribe(() => {
pages = getPages();
});
backendInUseStore.subscribe(() => {
pages = getPages();
});
page.subscribe((pageData) => {
playerIsPip = !pageData.url.pathname.includes('/watch');
@@ -1,14 +1,15 @@
<script lang="ts">
import { getFeed } from '$lib/api/index';
import type { PlaylistPageVideo, Video, VideoBase } from '$lib/api/model';
import { feedCacheStore } from '$lib/store';
import { feedCacheStore, feedLoadingStore } from '$lib/store';
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
import ItemsList from '$lib/components/ItemsList.svelte';
import { resolve } from '$app/paths';
import { _ } from '$lib/i18n';
import PageLoading from '$lib/components/PageLoading.svelte';
let currentPage = 1;
let videos: (VideoBase | Video | PlaylistPageVideo)[] = $state($feedCacheStore.subscription);
let videos: (VideoBase | Video | PlaylistPageVideo)[] = $derived($feedCacheStore.subscription);
async function loadMore(event: InfiniteEvent) {
currentPage++;
@@ -31,6 +32,9 @@
</nav>
<div class="space"></div>
<ItemsList items={videos ?? []} />
<InfiniteLoading on:infinite={loadMore} />
{#if !$feedLoadingStore}
<ItemsList items={videos ?? []} />
<InfiniteLoading on:infinite={loadMore} />
{:else}
<PageLoading />
{/if}
@@ -2,7 +2,7 @@ import { getFeed } from '$lib/api/index';
import type { PlaylistPageVideo, Video, VideoBase } from '$lib/api/model';
import { localDb } from '$lib/dexie';
import { authProtected, excludeDuplicateFeeds } from '$lib/misc';
import { feedCacheStore } from '$lib/store';
import { feedCacheStore, feedLoadingStore } from '$lib/store';
import { error } from '@sveltejs/kit';
import { get } from 'svelte/store';
@@ -40,17 +40,23 @@ export async function load() {
let videos = get(feedCacheStore).subscription;
if (!videos) {
let feeds;
try {
feeds = await getFeed(100, 1);
} catch (errorMessage: any) {
error(500, errorMessage);
}
feedCacheStore.set({ ...get(feedCacheStore), subscription: [] });
videos = await sortVideosByFavourites([...feeds.notifications, ...feeds.videos]);
feedCacheStore.set({ ...get(feedCacheStore), subscription: videos });
feedLoadingStore.set(true);
getFeed(100, 1)
.then(async (feed) => {
videos = await sortVideosByFavourites([...feed.notifications, ...feed.videos]);
feedCacheStore.set({ ...get(feedCacheStore), subscription: videos });
})
.catch((errorMsg) => {
error(500, errorMsg);
})
.finally(() => {
feedLoadingStore.set(false);
});
} else {
feedLoadingStore.set(false);
await getFeed(100, 1).then(async (feeds) => {
const newVideos = await sortVideosByFavourites([
...feeds.notifications,
-1
View File
@@ -1,2 +1 @@
declare module 'virtual:pwa-info';
declare module 'psl';
@@ -1,20 +1,18 @@
import { isOwnBackend } from '$lib/shared';
import psl from 'psl';
import { env } from '$env/dynamic/public';
import { error } from '@sveltejs/kit';
import { parse as tldParse } from 'tldts';
const allowedDomains: string[] = [
const allowedBaseDomains: string[] = [
'youtube.com',
'ytimg.com',
'googlevideo.com',
'returnyoutubedislike.com',
'sponsor.ajay.app',
'dearrow-thumb.ajay.app',
'materialio.us'
'ajay.app'
];
const dynamicAllowDomains = [
const dynamicAllowDomainsEnvVars = [
env.PUBLIC_DEFAULT_DEARROW_THUMBNAIL_INSTANCE,
env.PUBLIC_DEFAULT_DEARROW_INSTANCE,
env.PUBLIC_DEFAULT_INVIDIOUS_INSTANCE,
@@ -24,16 +22,17 @@ const dynamicAllowDomains = [
env.PUBLIC_DEFAULT_COMPANION_INSTANCE
];
dynamicAllowDomains.forEach((domain) => {
if (domain) {
allowedDomains.push(domain);
const dynamicAllowDomains: string[] = [];
for (const dynamicDomain of dynamicAllowDomainsEnvVars) {
if (dynamicDomain) {
dynamicAllowDomains.push(dynamicDomain.replace(/^https?:\/\//, ''));
}
});
}
async function proxyRequest(
request: Request,
urlToProxy: string,
captchaKey: string,
userId: string | undefined = undefined
): Promise<Response> {
const backendRestrictions = isOwnBackend();
@@ -53,7 +52,12 @@ async function proxyRequest(
throw error(400, 'Invalid URL');
}
if (!allowedDomains.includes(psl.parse(urlToProxyObj.host).domain)) {
const baseDomain = tldParse(urlToProxyObj.host).domain;
if (
!dynamicAllowDomains.includes(urlToProxyObj.host) &&
(!baseDomain || !allowedBaseDomains.includes(baseDomain))
) {
// allowAnyProxy allows a instance owner to bypass the whitelist.
// BUT is extremely strict.
// AND I still don't recommend this.
@@ -63,7 +67,7 @@ async function proxyRequest(
backendRestrictions.registrationAllowed ||
!userId
) {
throw error(400, 'Invalid URL');
throw error(400, 'URL not whitelisted');
}
}
@@ -114,20 +118,20 @@ async function proxyRequest(
}
export async function GET({ request, params, locals }) {
return await proxyRequest(request, params.urlToProxy, locals.captchaKey, locals.userId);
return await proxyRequest(request, params.urlToProxy, locals.userId);
}
export async function PATCH({ request, params, locals }) {
return await proxyRequest(request, params.urlToProxy, locals.captchaKey, locals.userId);
return await proxyRequest(request, params.urlToProxy, locals.userId);
}
export async function DELETE({ request, params, locals }) {
return await proxyRequest(request, params.urlToProxy, locals.captchaKey, locals.userId);
return await proxyRequest(request, params.urlToProxy, locals.userId);
}
export async function PUT({ request, params, locals }) {
return await proxyRequest(request, params.urlToProxy, locals.captchaKey, locals.userId);
return await proxyRequest(request, params.urlToProxy, locals.userId);
}
export async function POST({ request, params, locals }) {
return await proxyRequest(request, params.urlToProxy, locals.captchaKey, locals.userId);
return await proxyRequest(request, params.urlToProxy, locals.userId);
}
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.3"
LATEST_VERSION = "1.15.4"
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")