@@ -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">
|
||||
|
||||
Generated
+2
-2
@@ -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,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",
|
||||
|
||||
Generated
+3
-15
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
-1
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user