Merge pull request #1408 from Materialious/update/1.14.3

Update/1.14.3
This commit is contained in:
Ward
2026-02-13 03:13:02 +13:00
committed by GitHub
18 changed files with 191 additions and 81 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
# Features
- Support for using without a Invidious instance (With experimental Youtubejs backend).
- Use in local mode without relying on a Invidious instance.
- [Invidious companion support.](./docs/DOCKER.md#invidious-companion-support)
- [Invidious API extended integration.](https://github.com/Materialious/api-extended)
- [YouTube.js](https://github.com/LuanRT/YouTube.js) fallback if Invidious fails loading videos for Desktop & Android.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 206
versionName "1.14.2"
versionCode 207
versionName "1.14.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -8,6 +8,7 @@
<p>Materialious is a client for Invidious and YouTube using material design. It lets you browse and watch videos, manage playlists and visit channels.</p>
<p>Features:</p>
<ul>
<li>Use in local mode without relying on a Invidious instance</li>
<li>Invidious companion support</li>
<li>Invidious API extended integration</li>
<li>YouTube.js fallback if Invidious fails loading videos</li>
@@ -52,7 +53,7 @@
</screenshot>
<screenshot>
<caption>Modifying settings</caption>
<image>https://raw.githubusercontent.com/Materialious/Materialious/4baa7b897a46d4e71aaca7d322e4f7dafc870a33/previews/setting-preview.png</image>
<image>https://raw.githubusercontent.com/Materialious/Materialious/43e6eb78edae030ffd372af5b9a8dcf92564be46/previews/setting-preview.png</image>
</screenshot>
<screenshot>
<caption>Browsing a channel</caption>
@@ -69,8 +70,12 @@
<content_attribute id="social-contacts">intense</content_attribute>
</content_rating>
<releases>
<release version="1.14.2" date="2026-2-12">
<release version="1.14.3" date="2026-2-13">
<url>https://github.com/Materialious/Materialious/releases/tag/1.14.3</url>
</release>
<release version="1.14.2" date="2026-2-12">
<url>https://github.com/Materialious/Materialious/releases/tag/1.14.2</url>
</release>
<release version="1.14.1" date="2026-2-12">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.14.2",
"version": "1.14.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.14.2",
"version": "1.14.3",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.14.2",
"version": "1.14.3",
"description": "Modern material design for YouTube and Invidious.",
"author": {
"name": "Ward Pearce",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.14.2",
"version": "1.14.3",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
+11 -4
View File
@@ -274,8 +274,11 @@ export async function notificationsMarkAsRead(fetchOptions: RequestInit = {}) {
await fetchErrorHandle(await fetch(path, { ...buildAuthHeaders(), ...fetchOptions }));
}
export async function getSubscriptions(fetchOptions: RequestInit = {}): Promise<Subscription[]> {
if (isYTBackend()) {
export async function getSubscriptions(
fetchOptions: RequestInit = {},
bypassYTBackend: boolean = false
): Promise<Subscription[]> {
if (isYTBackend() && !bypassYTBackend) {
return getSubscriptionsYTjs();
}
const resp = await fetchErrorHandle(
@@ -304,8 +307,12 @@ export async function amSubscribed(
}
}
export async function postSubscribe(authorId: string, fetchOptions: RequestInit = {}) {
if (isYTBackend()) {
export async function postSubscribe(
authorId: string,
fetchOptions: RequestInit = {},
bypassYTBackend: boolean = false
) {
if (isYTBackend() && !bypassYTBackend) {
return postSubscribeYTjs(authorId);
}
@@ -1,5 +1,5 @@
import { localDb } from '$lib/dexie';
import { logoutStores } from '$lib/misc';
import { clearCaches } from '$lib/misc';
import { cleanNumber } from '$lib/numbers';
import { relativeTimestamp } from '$lib/time';
import { get } from 'svelte/store';
@@ -24,13 +24,19 @@ export async function amSubscribedYTjs(authorId: string): Promise<boolean> {
return (await localDb.channelSubscriptions.where('channelId').equals(authorId).count()) > 0;
}
export async function postSubscribeYTjs(authorId: string) {
const channel = await getChannelYTjs(authorId);
export async function postSubscribeYTjs(
authorId: string,
authorName: string | undefined = undefined
) {
if (!authorName) {
const channel = await getChannelYTjs(authorId);
authorName = channel.author;
}
try {
await localDb.channelSubscriptions.add({
channelId: authorId,
channelName: channel.author,
channelName: authorName,
lastRSSFetch: new Date(0)
});
} catch {
@@ -44,7 +50,7 @@ export async function postSubscribeYTjs(authorId: string) {
export async function deleteUnsubscribeYTjs(authorId: string) {
await localDb.channelSubscriptions.where('channelId').equals(authorId).delete();
await localDb.subscriptionFeed.where('authorId').equals(authorId).delete();
logoutStores();
clearCaches();
}
export async function parseChannelRSS(channelId: string): Promise<void> {
@@ -147,16 +153,17 @@ export async function getFeedYTjs(): Promise<Feed> {
await Promise.all(toUpdatePromises);
}
const videos = await localDb.subscriptionFeed.toArray();
let videos = await localDb.subscriptionFeed.toArray();
videos.sort((a, b) => b.published - a.published);
const cullAfter = get(engineCullYTStore);
if (videos.length > cullAfter) {
const videosToDelete = videos.slice(cullAfter);
const videoIdsToDelete = videosToDelete.map((video) => video.videoId);
await localDb.subscriptionFeed.where('videoId').anyOf(videoIdsToDelete).delete();
await localDb.subscriptionFeed.where('id').anyOf(videoIdsToDelete).delete();
// Don't display culled videos.
videos = videos.slice(0, cullAfter);
}
return {
@@ -1,9 +1,18 @@
<script lang="ts">
import { isYTBackend } from '$lib/misc';
import { _ } from '$lib/i18n';
import { engineCooldownYTStore, engineCullYTStore, engineFallbacksStore } from '$lib/store';
import {
authStore,
engineCooldownYTStore,
engineCullYTStore,
engineFallbacksStore,
instanceStore
} 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';
const engineFallbacks: EngineFallback[] = [
'Channel',
@@ -33,6 +42,52 @@
engineFallbacksStore.set(enabledFallbacks);
}
async function importInvidiousSubs() {
const importedSubs = await getSubscriptions({}, true);
addToast({
data: {
text: $_('layout.backendEngine.importingToMaterialious')
}
});
const subPromises: Promise<void>[] = [];
importedSubs.forEach((sub) => {
subPromises.push(postSubscribeYTjs(sub.authorId, sub.author));
});
await Promise.all(subPromises);
addToast({
data: {
text: $_('layout.backendEngine.importingToMaterialiousFinished')
}
});
}
async function exportMaterialiousSubs() {
const subs = await getSubscriptions();
addToast({
data: {
text: $_('layout.backendEngine.exportingToInvidious')
}
});
const subPromises: Promise<void>[] = [];
subs.forEach((sub) => {
subPromises.push(postSubscribe(sub.authorId, {}, true));
});
await Promise.all(subPromises);
addToast({
data: {
text: $_('layout.backendEngine.exportingToInvidiousFinished')
}
});
}
</script>
<article class="error-container">
@@ -40,7 +95,7 @@
</article>
{#if isYTBackend()}
<h4>Feed</h4>
<h6>Feed</h6>
<div class="field label prefix border">
<i>view_stream</i>
<input
@@ -65,8 +120,25 @@
/>
<label for="cull">{$_('layout.backendEngine.cooldown')}</label>
</div>
{#if $authStore && $instanceStore}
<h6>{$_('layout.backendEngine.importExport')}</h6>
<div class="space"></div>
<button onclick={exportMaterialiousSubs} class="surface-container-highest">
<i>upload</i>
<div>{$_('layout.backendEngine.exportToInvidious')}</div>
</button>
<div class="space"></div>
<button onclick={importInvidiousSubs} class="surface-container-highest">
<i>download</i>
<div>{$_('layout.backendEngine.importToMaterialious')}</div>
</button>
{/if}
{:else}
<h4>{$_('layout.backendEngine.fallbacks')}</h4>
<h6>{$_('layout.backendEngine.fallbacks')}</h6>
{#each engineFallbacks as fallback (fallback)}
<nav class="no-padding">
<div class="max">
@@ -11,7 +11,7 @@
import type { RgbaColor, HsvaColor, Colord } from 'colord';
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { ensureNoTrailingSlash, isMobile, logoutStores } from '../../misc';
import { ensureNoTrailingSlash, isMobile, clearCaches } from '../../misc';
import { getPages, type Pages } from '../../navPages';
import ColorPicker from 'svelte-awesome-color-picker';
import {
@@ -75,8 +75,8 @@
}
}
function clearPreviousInstance() {
logoutStores();
function reloadState() {
clearCaches();
ui('#dialog-settings');
goto(resolve('/', {}), { replaceState: true });
location.reload();
@@ -113,13 +113,14 @@
instanceStore.set(instance);
clearPreviousInstance();
reloadState();
authStore.set(null);
}
async function setBackend(event: Event) {
const select = event.target as HTMLSelectElement;
backendInUseStore.set(select.value as 'ivg' | 'yt');
clearPreviousInstance();
reloadState();
}
function allowInsecureRequests() {
@@ -129,14 +129,13 @@
<div class="grid">
<div class="s12 m4 l4 m l">
<div class="padding">
<div class="categories padding">
{#each tabs as tab, index (tab)}
<a
<button
class:active={isActive(tab.id)}
class:surface-container-lowest={isActive(tab.id)}
class:surface-container-highest={!isActive(tab.id)}
aria-selected={isActive(tab.id)}
class="button"
id={`tab-${tab.id}`}
aria-controls={`panel-${tab.id}`}
tabindex={isActive(tab.id) ? 0 : -1}
@@ -145,25 +144,26 @@
>
<i>{tab.icon}</i>
<span>{tab.label}</span>
</a>
</button>
<div class="space"></div>
{/each}
</div>
</div>
<div class="s12 m8 l8">
{#each tabs as tab (tab)}
<div
class="page padding"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={!isActive(tab.id)}
inert={!$isAndroidTvStore && !isActive(tab.id)}
aria-hidden={!isActive(tab.id)}
class:active={isActive(tab.id)}
>
<tab.component />
</div>
{/each}
<div class="settings padding">
{#each tabs as tab (tab)}
<div
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={!isActive(tab.id)}
inert={!$isAndroidTvStore && !isActive(tab.id)}
aria-hidden={!isActive(tab.id)}
class:active={isActive(tab.id)}
>
<tab.component />
</div>
{/each}
</div>
</div>
</div>
</div>
@@ -172,12 +172,20 @@
<style>
#dialog-settings {
width: 800px;
height: 800px;
}
.page {
max-height: 800px;
overflow: scroll;
.categories {
position: sticky;
top: 0;
height: 100%;
overflow: hidden;
height: fit-content;
}
.settings {
height: 800px;
overflow-y: auto;
padding-right: 10px;
}
.grid {
@@ -195,7 +203,7 @@
padding: 0;
}
a.button {
.categories button {
width: 100%;
box-sizing: border-box !important;
}
@@ -206,8 +214,8 @@
height: 100%;
}
.page {
max-height: 100%;
.settings {
height: 100%;
}
}
</style>
+8 -1
View File
@@ -179,7 +179,14 @@
"warning": "Advanced settings! For experienced users only. Do not change unless you know what youre doing.",
"cull": "Max feed items",
"cooldown": "Re-fetch channel hour cooldown",
"fallbacks": "Fallbacks"
"fallbacks": "Fallbacks",
"importExport": "Import/Export subscriptions",
"exportToInvidious": "Export to Invidious from Materialious",
"importToMaterialious": "Import from Invidious to Materialious",
"importingToMaterialious": "Importing to Materialious began",
"importingToMaterialiousFinished": "Importing to Materialious finished",
"exportingToInvidious": "Exporting to Invidious began",
"exportingToInvidiousFinished": "Exporting to Invidious finished"
},
"player": {
"title": "Player",
+1 -2
View File
@@ -233,8 +233,7 @@ export function isYTBackend(): boolean {
return get(backendInUseStore) === 'yt' && Capacitor.isNativePlatform();
}
export function logoutStores() {
authStore.set(null);
export function clearCaches() {
feedCacheStore.set({});
searchCacheStore.set({});
playlistCacheStore.set({});
+8 -1
View File
@@ -1,12 +1,13 @@
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { isYTBackend } from './misc';
import { authStore } from './store';
export type Pages = { icon: string; href: string; name: string; requiresAuth: boolean }[];
// Must be a func do to how i18n is loaded
export function getPages(): Pages {
return [
let pages: Pages = [
{
icon: 'home',
href: !isYTBackend() ? '/' : '/subscriptions',
@@ -26,4 +27,10 @@ export function getPages(): Pages {
requiresAuth: true
}
];
pages = pages.filter((page) => {
return !page.requiresAuth || (get(authStore) && !isYTBackend());
});
return pages;
}
+1 -1
View File
@@ -315,7 +315,7 @@ export const deArrowThumbnailInstanceStore = persist(
);
export const engineCullYTStore: Writable<number> = persist(
writable(1000),
writable(500),
createStorage(),
'engineCullYT'
);
+17 -20
View File
@@ -39,7 +39,7 @@
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { pwaInfo } from 'virtual:pwa-info';
import { isYTBackend, logoutStores, truncate } from '$lib/misc';
import { isYTBackend, clearCaches, truncate } from '$lib/misc';
import Author from '$lib/components/Author.svelte';
import Toast from '$lib/components/Toast.svelte';
@@ -179,7 +179,8 @@
}
function logout() {
logoutStores();
authStore.set(null);
clearCaches();
goto(resolve('/', {}));
}
@@ -236,7 +237,7 @@
setTheme();
setAmoledTheme();
if (isLoggedIn) {
if (isLoggedIn && !isYTBackend()) {
loadNotifications().catch(() => authStore.set(null));
}
@@ -269,12 +270,10 @@
</a>
{/if}
{#each getPages() as navPage (navPage)}
{#if !navPage.requiresAuth || isLoggedIn}
<a href={resolve(navPage.href, {})} class:active={$page.url.href.endsWith(navPage.href)}
><i>{navPage.icon}</i>
<div>{navPage.name}</div>
</a>
{/if}
<a href={resolve(navPage.href, {})} class:active={$page.url.href.endsWith(navPage.href)}
><i>{navPage.icon}</i>
<div>{navPage.name}</div>
</a>
{/each}
{#if $isAndroidTvStore}
<div class="divider"></div>
@@ -345,7 +344,7 @@
<i class:primary-text={$syncPartyPeerStore}>group</i>
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
</button>
{#if isLoggedIn}
{#if isLoggedIn && !isYTBackend()}
<button
class="circle large transparent"
onclick={() => {
@@ -387,16 +386,14 @@
<nav class="bottom s">
{#each getPages() as navPage (navPage)}
{#if !navPage.requiresAuth || isLoggedIn}
<a
class="round"
href={resolve(navPage.href, {})}
class:active={$page.url.href.endsWith(navPage.href)}
data-sveltekit-preload-data="off"
><i>{navPage.icon}</i>
<span style="font-size: .8em;">{navPage.name}</span>
</a>
{/if}
<a
class="round"
href={resolve(navPage.href, {})}
class:active={$page.url.href.endsWith(navPage.href)}
data-sveltekit-preload-data="off"
><i>{navPage.icon}</i>
<span style="font-size: .8em;">{navPage.name}</span>
</a>
{/each}
</nav>
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.14.2"
LATEST_VERSION = "1.14.3"
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")