From aba30238b6d14376e2501ffd449e821a74980e36 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 8 Oct 2025 19:20:41 +1300 Subject: [PATCH 1/2] Improve android fetch --- materialious/android/app/build.gradle | 4 +-- .../electron/materialious.metainfo.xml | 6 +++- materialious/electron/package.json | 2 +- materialious/package.json | 2 +- .../src/lib/android/http/androidRequests.ts | 32 +++++++++---------- .../src/lib/components/Settings/About.svelte | 6 ---- materialious/src/lib/misc.ts | 4 +++ update_versions.py | 2 +- 8 files changed, 30 insertions(+), 28 deletions(-) diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 8557a778..171c0631 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 154 - versionName "1.10.13" + versionCode 155 + versionName "1.10.14" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index eac2a1f5..04c82877 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -71,7 +71,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.10.14 + + https://github.com/Materialious/Materialious/releases/tag/1.10.13 diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 2ef9144a..8e3769b8 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.10.13", + "version": "1.10.14", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package.json b/materialious/package.json index 91c42b3c..7ed0b774 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.10.13", + "version": "1.10.14", "private": true, "scripts": { "dev": "vite dev", diff --git a/materialious/src/lib/android/http/androidRequests.ts b/materialious/src/lib/android/http/androidRequests.ts index 21d549c4..24d6fada 100644 --- a/materialious/src/lib/android/http/androidRequests.ts +++ b/materialious/src/lib/android/http/androidRequests.ts @@ -1,12 +1,12 @@ -import { goto } from '$app/navigation'; -import { isAndroidTvStore } from '$lib/store'; +import { timeout } from '$lib/misc'; import { Capacitor } from '@capacitor/core'; import { NodeJS } from 'capacitor-nodejs'; -import { get } from 'svelte/store'; const originalFetch = window.fetch; const corsProxyUrl: string = 'http://localhost:3000/'; +let nodejsStarted = false; + function needsProxying(target: string): boolean { if (!target.startsWith('http')) return false; return true; @@ -16,6 +16,19 @@ export const androidFetch = async ( requestInput: string | URL | Request, requestOptions?: RequestInit ): Promise => { + // On initial request pause until OPTIONS request passes on local proxy, only reliable way + // to ensure proxy is working on android. + if (!nodejsStarted) { + let testResp: Response | undefined = undefined; + while (typeof testResp === 'undefined' || !testResp.ok) { + try { + testResp = await originalFetch(corsProxyUrl, { method: 'OPTIONS' }); + } catch (error) {} + await timeout(100); + } + nodejsStarted = true; + } + const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString(); if (needsProxying(uri)) { @@ -54,17 +67,4 @@ if (Capacitor.getPlatform() === 'android') { /* @ts-ignore */ return originalXhrOpen.apply(this, args); }; - - NodeJS.whenReady().then(() => { - goto('/', { replaceState: true }); - }); - - // Required for Android TV to load correctly. - let hasReloaded = false; - isAndroidTvStore.subscribe((isAndroidTv) => { - if (hasReloaded || !isAndroidTv) return; - hasReloaded = true; - - setTimeout(() => goto('/', { replaceState: true }), 2000); - }); } diff --git a/materialious/src/lib/components/Settings/About.svelte b/materialious/src/lib/components/Settings/About.svelte index da909131..6d1fffe5 100644 --- a/materialious/src/lib/components/Settings/About.svelte +++ b/materialious/src/lib/components/Settings/About.svelte @@ -16,12 +16,6 @@ }); -

- Materialious is a modern material design frontend for Invidious, focused on a clean, - privacy-friendly YouTube experience. It supports local video fallback when Invidious fails and is - available on Web, Desktop, Android, and Android TV. -

-
diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index 914d654c..2ed670a6 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -140,3 +140,7 @@ export function createVideoUrl(videoId: string, playlistId: string): URL { return watchUrl; } + +export function timeout(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/update_versions.py b/update_versions.py index a089536a..536f82ed 100644 --- a/update_versions.py +++ b/update_versions.py @@ -3,7 +3,7 @@ import os import re from datetime import datetime -LATEST_VERSION = "1.10.13" +LATEST_VERSION = "1.10.14" 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") From 97382e9a437c356890ec8b5b29a9181114e74529 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 8 Oct 2025 19:56:54 +1300 Subject: [PATCH 2/2] Minor improvements to android tv watch page --- .../src/lib/components/ItemsList.svelte | 57 +++--- materialious/src/routes/(app)/+layout.svelte | 8 +- .../routes/(no-layout)/tv/[slug]/+page.svelte | 162 ++++++++---------- 3 files changed, 108 insertions(+), 119 deletions(-) diff --git a/materialious/src/lib/components/ItemsList.svelte b/materialious/src/lib/components/ItemsList.svelte index 3e7cd93e..fc456c3f 100644 --- a/materialious/src/lib/components/ItemsList.svelte +++ b/materialious/src/lib/components/ItemsList.svelte @@ -13,7 +13,7 @@ } from '../api/model'; import { authStore, feedLastItemId, isAndroidTvStore } from '../store'; import ContentColumn from './ContentColumn.svelte'; - import { onMount, onDestroy } from 'svelte'; + import { onMount, onDestroy, tick } from 'svelte'; import Mousetrap from 'mousetrap'; import { extractUniqueId } from '$lib/misc'; import ChannelThumbnail from './ChannelThumbnail.svelte'; @@ -26,12 +26,18 @@ | PlaylistPage[]; playlistId?: string; playlistAuthor?: string; + classes?: string; } - let { items = [], playlistId = '', playlistAuthor = '' }: Props = $props(); + let { + items = [], + playlistId = '', + playlistAuthor = '', + classes = 'page right active' + }: Props = $props(); - let gridElement = $state(); - let focusableItems = $state([]); + let gridElement: HTMLElement; + let focusableItems: HTMLElement[] = []; let currentFocusIndex = $state(0); let lastFocusIndex = $state(0); // Remember position when leaving grid let columns = $state(4); // Default columns for Android TV @@ -65,6 +71,7 @@ } function setupAndroidTVNavigation() { + console.log('gridElement', gridElement); if (!$isAndroidTvStore || !gridElement) return; focusableItems = Array.from( @@ -151,7 +158,7 @@ return true; } - onMount(() => { + onMount(async () => { if ($feedLastItemId && !$isAndroidTvStore) { document .getElementById($feedLastItemId) @@ -161,30 +168,28 @@ } if ($isAndroidTvStore) { + await tick(); // Setup Android TV navigation - setTimeout(() => { - setupAndroidTVNavigation(); - // Focus the correct item initially (first time or restored position) - if (focusableItems.length > 0) { - let focusedItemIndex = -1; - if ($feedLastItemId) { - focusedItemIndex = focusableItems.findIndex((item) => item.id === $feedLastItemId); - feedLastItemId.set(undefined); + setupAndroidTVNavigation(); + + // Focus the correct item initially (first time or restored position) + if (focusableItems.length > 0) { + if ($feedLastItemId) { + const focusedItemIndex = focusableItems.findIndex((item) => item.id === $feedLastItemId); + feedLastItemId.set(undefined); + + if (focusedItemIndex !== -1) { + focusableItems[focusedItemIndex]?.focus(); + currentFocusIndex = focusedItemIndex; } - const focusIndex = - focusedItemIndex === -1 - ? Math.min(lastFocusIndex, focusableItems.length - 1) - : focusedItemIndex; - focusableItems[focusIndex]?.focus(); - currentFocusIndex = focusIndex; } - }, 200); + } // Bind navigation keys - Mousetrap.bind('up', (e) => handleNavigation('up', e)); + Mousetrap.bind('up', (e) => handleNavigation('up', e), 'keydown'); Mousetrap.bind('down', (e) => handleNavigation('down', e), 'keydown'); - Mousetrap.bind('left', (e) => handleNavigation('left', e)); - Mousetrap.bind('right', (e) => handleNavigation('right', e)); + Mousetrap.bind('left', (e) => handleNavigation('left', e), 'keydown'); + Mousetrap.bind('right', (e) => handleNavigation('right', e), 'keydown'); // Watch for window resize to recalculate columns window.addEventListener('resize', calculateColumns); @@ -207,14 +212,12 @@ // Update navigation when items change $effect(() => { if ($isAndroidTvStore && items.length > 0 && gridElement) { - setTimeout(() => { - setupAndroidTVNavigation(); - }, 100); + tick().then(() => setupAndroidTVNavigation()); } }); -
+
{#each items as item, index} diff --git a/materialious/src/routes/(app)/+layout.svelte b/materialious/src/routes/(app)/+layout.svelte index 28bacdeb..f2fe1070 100644 --- a/materialious/src/routes/(app)/+layout.svelte +++ b/materialious/src/routes/(app)/+layout.svelte @@ -35,7 +35,7 @@ import 'beercss'; import ui from 'beercss'; import 'material-dynamic-colors'; - import { onMount } from 'svelte'; + import { onDestroy, onMount } from 'svelte'; import { _ } from '$lib/i18n'; import { get } from 'svelte/store'; import { pwaInfo } from 'virtual:pwa-info'; @@ -241,6 +241,12 @@ } }); + onDestroy(() => { + if ($isAndroidTvStore) { + Mousetrap.unbind('down', 'keyup'); + } + }); + let webManifestLink = $derived(pwaInfo ? pwaInfo.webManifest.linkTag : ''); diff --git a/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte b/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte index e7d13160..a8badda7 100644 --- a/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte +++ b/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte @@ -11,6 +11,7 @@ import { _ } from '$lib/i18n'; import { playlistCacheStore } from '$lib/store.js'; import { fade } from 'svelte/transition'; + import ItemsList from '$lib/components/ItemsList.svelte'; let { data } = $props(); @@ -29,64 +30,84 @@ }); } - Mousetrap.bind('down', () => { - if (showInfo) return true; + Mousetrap.bind( + 'down', + () => { + if (showInfo) return true; - showInfo = true; - tick().then(() => { - document.getElementById('shown-info')?.focus(); - }); - - return false; - }); - - Mousetrap.bind('up', () => { - const infoElement = document.getElementById('shown-info'); - - if (showInfo && infoElement) { - if (infoElement.scrollTop === 0) { - showInfo = false; - return false; - } - return true; - } - - if (!showInfo) { showInfo = true; tick().then(() => { document.getElementById('shown-info')?.focus(); }); + return false; - } + }, + 'keyup' + ); - return true; - }); + Mousetrap.bind( + 'up', + () => { + const infoElement = document.getElementById('shown-info'); - Mousetrap.bind('right', () => { - if (!playerElement || showInfo) return true; - playerElement.currentTime = playerElement.currentTime + 10; - return false; - }); - - Mousetrap.bind('left', () => { - if (!playerElement || showInfo) return true; - - playerElement.currentTime = playerElement.currentTime - 10; - return false; - }); - - Mousetrap.bind('enter', () => { - if (!showInfo) { - if (playerElement?.paused) { - playerElement?.play(); - } else { - playerElement?.pause(); + if (showInfo && infoElement) { + if (infoElement.scrollTop === 0) { + showInfo = false; + return false; + } + return true; } - return false; - } - return true; - }); + if (!showInfo) { + showInfo = true; + tick().then(() => { + document.getElementById('shown-info')?.focus(); + }); + return false; + } + + return true; + }, + 'keyup' + ); + + Mousetrap.bind( + 'right', + () => { + if (!playerElement || showInfo) return true; + playerElement.currentTime = playerElement.currentTime + 10; + return false; + }, + 'keyup' + ); + + Mousetrap.bind( + 'left', + () => { + if (!playerElement || showInfo) return true; + + playerElement.currentTime = playerElement.currentTime - 10; + return false; + }, + 'keyup' + ); + + Mousetrap.bind( + 'enter', + () => { + if (!showInfo) { + if (playerElement?.paused) { + playerElement?.play(); + } else { + playerElement?.pause(); + } + return false; + } + + return true; + }, + 'keyup' + ); }); onDestroy(() => { @@ -137,52 +158,11 @@ {#if data.playlistId && data.playlistId in $playlistCacheStore}
{$_('playlistVideos')}
- -
- {#each $playlistCacheStore[data.playlistId].videos as playlistVideo} - -
{ - showInfo = false; - }} - role="presentation" - id={playlistVideo.videoId} - class:border={playlistVideo.videoId === data.video.videoId} - > - {#key playlistVideo.videoId} - - {/key} -
-
- {/each} -
+ {/if} {#if data.video.recommendedVideos.length > 0}
{$_('recommendedVideos')}
-
- {#each data.video.recommendedVideos as recommendedVideo} - -
{ - showInfo = false; - }} - role="presentation" - style="height: 100%;" - class="no-padding" - > - {#key recommendedVideo.videoId} - - {/key} -
-
- {/each} -
+ {/if} {/if}